Android許可權適配

當C遇上java發表於2018-02-08

一、動態許可權的開始

Android系統目前已經更新到8.0(O),自從Android6.0(M)開始,許可權適配問題成為我們開發適配工作之一。在6.0之前,應用申請許可權只需要將許可權在AndroidManifest中註冊宣告即可,使用者安裝應用時會有一個許可權列表,只有同意這些許可權後,我們才能安裝成功,這就使使用者不得不忍受一些無理的許可權要求(訪問通訊錄、訪問簡訊等);在6.0之後,Google出去安全隱私考慮,引入執行時許可權,可見官網:執行時許可權 將系統許可權分為兩類,一類是Normal permissions,這類許可權在一般不涉及使用者隱私,是不需要使用者授權,使用者安裝app時預設授予,另一類是dangerous permissions,這類許可權一般涉及到使用者的隱私,需要使用者先進行授權。 危險許可權是分組的,在Android6.0的機器上,基於授權機制,如果使用者申請了某個危險許可權,假如app已經申請過同組的其他危險許可權,那麼系統會立即授權,而不需要使用者點選,比如,你已經申請過 permission:android.permission.READ_EXTERNAL_STORAGE許可權,那麼當你再次申請permission:android.permission.WRITE_EXTERNAL_STORAGE許可權時,系統會自動授權,而不再由使用者確認。(對於危險許可權,我們在授權時最好用到一個申請一個,不要依賴這個機制,因為8.0對此機制已經修改,後面在說)

  • Dangerous Permissions許可權列表:

developer.android.com/guide/topic…

	group:android.permission-group.CONTACTS
	  permission:android.permission.WRITE_CONTACTS
	  permission:android.permission.GET_ACCOUNTS
	  permission:android.permission.READ_CONTACTS
	
	group:android.permission-group.PHONE
	  permission:android.permission.READ_CALL_LOG
	  permission:android.permission.READ_PHONE_STATE
	  permission:android.permission.CALL_PHONE
	  permission:android.permission.WRITE_CALL_LOG
	  permission:android.permission.USE_SIP
	  permission:android.permission.PROCESS_OUTGOING_CALLS
	  permission:com.android.voicemail.permission.ADD_VOICEMAIL
	
	group:android.permission-group.CALENDAR
	  permission:android.permission.READ_CALENDAR
	  permission:android.permission.WRITE_CALENDAR
	
	group:android.permission-group.CAMERA
	  permission:android.permission.CAMERA
	
	group:android.permission-group.SENSORS
	  permission:android.permission.BODY_SENSORS
	
	group:android.permission-group.LOCATION
	  permission:android.permission.ACCESS_FINE_LOCATION
	  permission:android.permission.ACCESS_COARSE_LOCATION
	
	group:android.permission-group.STORAGE
	  permission:android.permission.READ_EXTERNAL_STORAGE
	  permission:android.permission.WRITE_EXTERNAL_STORAGE
	
	group:android.permission-group.MICROPHONE
	  permission:android.permission.RECORD_AUDIO
	
	group:android.permission-group.SMS
	  permission:android.permission.READ_SMS
	  permission:android.permission.RECEIVE_WAP_PUSH
	  permission:android.permission.RECEIVE_MMS
	  permission:android.permission.RECEIVE_SMS
	  permission:android.permission.SEND_SMS
	  permission:android.permission.READ_CELL_BROADCASTS
複製程式碼

二、動態許可權的檢測和申請

由於執行時許可權的出現,我們必須對新的app做適配,當然,我們不必擔心之前的app,Google對此做了相容。當我們的應用targetSdkVersion小於23時,即使是裝在6.0的手機上,也會依然使用6.0之前的許可權規則,當targetSdkVersion大於23時,我們才使用新的這套執行時許可權規則。 許可權檢測: (這裡我就不說第三方許可權檢測庫的使用了.)

  /*
     *  1.檢查許可權是否存在
     *  2.(1)若許可權存在:直接開啟app
     *    (2)若許可權不存在,請求改許可權
     *  3.重寫onRequestPermissionsResult()方法,判斷是許可權返回結果,在做處理
     */
    private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            //有許可權:開啟相機
            AlbumBlock.doActionImageCapture(this);
        } else {
            //無許可權:申請許可權
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }


 /*開啟相機*/
    public static void doActionImageCapture(Activity activity) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        File file = getOutputMediaFile(activity, false);

        if (file == null) {
            ToastUtil.makeToast(activity, "無法儲存圖片");
            return;
        }

        Uri imgUri = Uri.fromFile(file);
        if (imgUri != null) {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
            activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
        } else {
            ToastUtil.makeToast(activity, "SD卡不可用,相機照片無法儲存!");
        }

    }


複製程式碼

三、許可權申請的回撥

當我們申請許可權後,需要對使用者操作的結果進行處理。一般我們會重寫onRequestPermissionsResult()方法。在這裡需要注意的是,一旦當第二次拒絕許可權時,會有一個是否詢問的勾選按鈕,一旦使用者選擇了不在詢問,那麼ActivityCompat.requestPermissions()方法將不在起作用,這個時候ActivityCompat.shouldShowRequestPermissionRationale()方法會返回false,我們可以通過這個判斷進行一些自己的處理。

 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (grantResults.length == 0) {
            return;
        }
        switch (requestCode) {
            case REQUEST_CAMERA:
                for (int i = 0; i < permissions.length; i++) {
                    String perm = permissions[i];
                    //依次判斷許可權
                    if (Manifest.permission.CAMERA.equals(perm)) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            //開啟相機
                            AlbumBlock.doActionImageCapture(this);
                        } else {
                            //拒絕
                            /*如果使用者選擇了拒絕,並且選擇了不在提醒,那麼這個方法會返回false,這樣我們就可以做一些自己的
                            * 提醒,避免一些不好的體驗。這個時候requestPermissions()是不起作用的,所以我們需要告訴使用者怎麼開啟許可權。
                            * */
                            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, perm)) {
                                showRequstPermissionDialog();
                            }
                        }
                    }
                }
                break;
        }

	  private void showRequstPermissionDialog() {
	        new AlertDialogUtil(this)
	                .setTitle("相機許可權未開啟")
	                .setMessage("請在設定中開啟相機許可權")
	                .setNegativeButton("暫不", null)
	                .setPositiveButton("去設定", new AlertDialogUtil.OnClickListener() {
	                    @Override
	                    public void onClick(DialogInterface dialog) {
	                        try {
 							/*
                            * 當然在這裡我們可以針對不同的手機品牌,跳轉到不同設定介面。
                            * 後面會給出相應的工具類。
                            */
	                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
	                            Uri uri = Uri.fromParts("package", getPackageName(), null);
	                            intent.setData(uri);
	                            startActivityForResult(intent, REQUEST_CODE_SETTINGS);
	                        } catch (Exception e) {
	
	                        }
	                    }
	                }).show();
	
	    }

複製程式碼

四、7.0許可權的更改及FileProvider的使用

我們知道Android6.0引入了執行時許可權"Runtime Permissions",那麼Android7.0對於許可權這塊,則增加了"StrictMode API 政策"。詳見官網:developer.android.com/about/versi… (在這裡推薦一篇介紹適配7.0的部落格:www.jianshu.com/p/56b9fb319…)7.0之後,如果一項包含檔案 file:// URI型別 的 Intent 離開了我們應用,那麼我們的APP會出故障,並出現 FileUriExposedException 異常,例如呼叫系統相機拍照或者裁剪照片。對於這個問題,可根據官網給出的方法去解決:

要在應用間共享檔案,您應傳送一項 content:// URI,並授予 URI 臨時訪問許可權。進行此授權的最
簡單方式是使用 FileProvider 類。如需瞭解有關許可權和共享檔案的詳細資訊,請參閱共享檔案。

複製程式碼

7.0之前開啟相機的方式可參照上面的程式碼,這個方式在7.0系統的手機由於"StrictMode API 政策禁"會報錯,所以我們需要通過 FileProvider來解決這個問題。下面是具體使用步驟(參考上面那篇部落格所寫):

第一步:在AndroidManifest 功能清單檔案中註冊Provider###

 <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.ff.app.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
複製程式碼

第二步:指定共享目錄

我們需要再res目錄下建立一個xml目錄,然後建立一個file_paths(這個名字只要和你註冊時使用的一致就行)檔案。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="files_path" path="images/" /> //相當 Context.getFilesDir() + path, name是分享url的一部分

    <cache-path name="cache_path" path="path/" /> //getCacheDir()

    <external-path name="external_path" path="ffapp/images/" /> //Environment.getExternalStorageDirectory()

    <external-files-path name="external_files" path="path/" />//getExternalFilesDir(String) Context.getExternalFilesDir(null)

    <external-cache-path name="external_cache" path="images/" /> //Context.getExternalCacheDir()

</paths>
複製程式碼

上述程式碼中path="",是有特殊意義的,它代表根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了,如果你將path設為path="images/", 那麼它代表著根目錄下的images下的目錄(eg:/storage/emulated/0/images/),如果你向其它應用分享images下目錄範圍之外的檔案是不行的。

第三步:使用FileProvider

 /*開啟相機*/
    public static void doActionImageCapture(Activity activity) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        File file = getOutputMediaFile(activity, false);

        if (file == null) {
            ToastUtil.makeToast(activity, "無法儲存圖片");
            return;
        }

        Uri imgUri;
		/*這裡就是對FileProvider的使用*/
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            imgUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", file);
            //將儲存圖片的Uri讀寫許可權授權給拍照工具應用
            List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
            for (ResolveInfo resolveInfo : resInfoList) {
                String packageName = resolveInfo.activityInfo.packageName;
				//新增這一句表示對目標應用臨時授權該Uri所代表的檔案
                activity.grantUriPermission(packageName, imgUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
        } else {
            imgUri = Uri.fromFile(file);
        }
        if (imgUri != null) {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
            activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
        } else {
            ToastUtil.makeToast(activity, "SD卡不可用,相機照片無法儲存!");
        }

    }

複製程式碼

在上面程式碼中我們判斷了系統版本,如果是7.0之後的,我們則將Uri轉換為一個content型別的Uri,並且對目標應用(相機)臨時授權該Uri所代表的檔案。getUriForFile()方法的中authority引數就是我們註冊時 android:authorities="com.ff.app.fileprovider"指定的值。這裡使用activity.getPackageName()是為了便於模組的抽離。

好了這就是7.0的FileProvider簡單使用。

五、8.0許可權的更改

在 Android 8.0 之前,如果應用在執行時請求許可權並且被授予該許可權,系統會"錯誤"地將屬於同一許可權組並且在清單中註冊的其他許可權也一起授予應用。(在說6.0許可權時我們說到了這個機制,並說過不要依賴,這裡8.0已經修復了)

對於針對 Android 8.0 的應用,此行為已被糾正。系統只會授予應用明確請求的許可權。然而,一旦使用者為應用授予某個許可權,則所有後續對該許可權組中許可權的請求都將被自動批准。

這意味著,我們應用中用到的所有許可權必須一個一個的宣告,舉個例子:在8.0之前,我們在申請 Manifest.permission.WRITE_EXTERNAL_STORAGE的時候,系統會自動對同一個許可權組的Manifest.permission.READ_EXTERNAL_STORAGE進行授權,而我們不需要再去申請,但是放到8.0及之後的系統上,如果app中同時用到這兩個許可權而我們只申請了一個的話,系統會報錯。

對於開發者來說,我們需要明確的知道我們的應用涉及到了哪些危險許可權,並且在使用時對於其進行授權申請。 Android 8.0 行為變更

六、開發中遇到的問題

關於許可權的適配暫時說到這裡,下面說一個開發中遇到的奇怪問題,暫時沒有解決。在開發內部員工OA的app時用到了掃一掃的功能,一般掃一掃肯定需要相機許可權,所以按照許可權的適配去判斷後,發現下面這段程式碼總是返回有許可權,無論許可權是授權的還是禁止的。(原因暫時不確定)

 private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            //有許可權:開啟相機
            AlbumBlock.doActionImageCapture(this);
        } else {
            //無許可權:申請許可權
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }
複製程式碼

那這樣檢測不就沒有用嗎,的確是的。後來看了很多資料,覺得可能是Android系統過於碎片化的原因,那麼怎麼去解決呢?首先我們需要換一種思路了,既然是呼叫相機,那麼我們看看相機是否可用不就行了?所以在檢測相機許可權的時候我們在增加一個方法。

 private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
			if(isCameraCanUse()){
				//有許可權:開啟相機
            	AlbumBlock.doActionImageCapture(this);
			}else{
				//去設定介面:下面會提供一個去大部分主流手機系統的設定介面的工具類
				
			}
            
        } else {
            //無許可權:申請許可權
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }

 /**
     * 測試當前攝像頭能否被使用
     *
     * @return
     */
    public static boolean isCameraCanUse() {
        boolean canUse = true;
        Camera mCamera = null;
        try {
            mCamera = Camera.open(0);
            mCamera.setDisplayOrientation(90);
        } catch (Exception e) {
            canUse = false;
        }
        if (canUse) {
            mCamera.release();
            mCamera = null;
        }
        //Timber.v("isCameraCanuse="+canUse);
        return canUse;
    }
複製程式碼

問題解決了,這裡提供一個博主提供的工具類,感覺是一個厲害的工具類。 這裡貼出網址:github.com/SenhLinsh/U…裡面有一個OSUtils可以判斷各大手機品牌的系統,還有一個AppUtils中有個方法,這列貼出來,配合使用。。

/**
     * 跳轉: 「許可權設定」介面
     * <p>
     * 根據各大廠商的不同定製而跳轉至其許可權設定
     * 目前已測試成功機型: 小米V7V8V9, 華為, 三星, 錘子, 魅族; 測試失敗: OPPO
     *
     * @return 成功跳轉許可權設定, 返回 true; 沒有適配該廠商或不能跳轉, 則自動預設跳轉設定介面, 並返回 false
     */
    public static Intent getPermissionSettingIntent(Activity activity) {
        Intent intent = new Intent();
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        OSUtils.ROM romType = OSUtils.getRomType();
        switch (romType) {
            case EMUI: // 華為
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"));
                break;
            case Flyme: // 魅族
                intent.setAction("com.meizu.safe.security.SHOW_APPSEC");
                intent.addCategory(Intent.CATEGORY_DEFAULT);
                intent.putExtra("packageName", activity.getPackageName());
                break;
            case MIUI: // 小米
                String rom = getMiuiVersion();
                if ("V6".equals(rom) || "V7".equals(rom)) {
                    intent.setAction("miui.intent.action.APP_PERM_EDITOR");
                    intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
                    intent.putExtra("extra_pkgname", activity.getPackageName());
                } else if ("V8".equals(rom) || "V9".equals(rom)) {
                    intent.setAction("miui.intent.action.APP_PERM_EDITOR");
                    intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
                    intent.putExtra("extra_pkgname", activity.getPackageName());
                } else {
                    intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                            .setData(Uri.parse("package:" + activity.getPackageName()));
                }
                break;
            case Sony: // 索尼
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity"));
                break;
            case ColorOS: // OPPO
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.PermissionManagerActivity"));
                break;
            case EUI: // 樂視
                intent.putExtra("packageName", activity.getPackageName());
                intent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps"));
                break;
            case LG: // LG
                intent.setAction("android.intent.action.MAIN");
                intent.putExtra("packageName", activity.getPackageName());
                ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
                intent.setComponent(comp);
                break;
            case SamSung: // 三星
            case SmartisanOS: // 錘子
                intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                        .setData(Uri.parse("package:" + activity.getPackageName()));
                break;
            default:
                intent.setAction(Settings.ACTION_SETTINGS);
                break;
        }
        return intent;
    }


    /**
     * 獲取 MIUI 版本號
     */
    private static String getMiuiVersion() {
        String propName = "ro.miui.ui.version.name";
        String line;
        BufferedReader input = null;
        try {
            Process p = Runtime.getRuntime().exec("getprop " + propName);
            input = new BufferedReader(
                    new InputStreamReader(p.getInputStream()), 1024);
            line = input.readLine();
            input.close();
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return line;
    }
複製程式碼

這個問題暫時這麼解決吧,能力不夠還得繼續學習!!

關於程式碼:傳送門

相關文章