[Android]透過SAF取得外接SDCard操作權限

前言

在Android中,SDCard有分成兩種

  1. 手機內附SDCard
  2. 外接SDCard

如果是第一種,只需要在AndroidManifest.xml中加入以下權限即可操作

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

對於第二種,4.4(KitKat)版之後因GOOGLE政策關係

已經拒絕第三方APP在data資料夾外做操作,原因眾說紛紜

但在5.0(Lollipop)版後,大概是因為很多開發者的抱怨(?)

GOOGLE擴充了SAF(Storage Access Framework)的功能

使得開發者又可以對SDCard為所欲為了(?)

Step 1

static final int DIRECTORY_CHOOSE_REQ_CODE = 42;
  
private void performDirectoryChoose(){
    Intent intent  = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, DIRECTORY_CHOOSE_REQ_CODE);
}
  
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    
    switch (requestCode){
        case DIRECTORY_CHOOSE_REQ_CODE:
            if (resultCode == Activity.RESULT_OK) {
                Uri sdCardUri = data.getData();
                grantUriPermission(
                    getPackageName(), 
                    sdCardUri, 
                    Intent.FLAG_GRANT_READ_URI_PERMISSION | 
                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                );
                  
                getContentResolver().
                    takePersistableUriPermission(
                        sdCardUri, 
                        Intent.FLAG_GRANT_READ_URI_PERMISSION | 
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    );
            }
            break;
    }
}

呼叫performDirectoryChoose後,按照下列步驟選擇

01

02

03

04

選擇完畢之後系統會去呼叫覆寫的onActivityResult方法

Uri sdCardUri = data.getData();

這一行是取到剛剛使用者所選擇的Uri

非絕對位置 Ex. /tree/3461-3532

grantUriPermission(
    getPackageName(), 
    sdCardUri, 
    Intent.FLAG_GRANT_READ_URI_PERMISSION | 
    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);

這一行是要求上面Uri的權限,這邊是要求讀跟寫

getContentResolver().takePersistableUriPermission(
    sdCardUri, 
    Intent.FLAG_GRANT_READ_URI_PERMISSION | 
    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);

這一行是讓權限永久化,如此一來就算手機重開機後也不會遺失權限

但換了一張SDCard還是得要從來一遍

Step 2

private DocumentFile df;

private boolean checkHasSDCard1PermissionAndSetParam(){
    DocumentFile df;
    UriPermission temp = getContentResolver().getPersistedUriPermissions();

    for (UriPermission p: temp) {
        df = DocumentFile.fromTreeUri(this, p.getUri());

        if (df.canRead() && "sdcard1".equals(df.getName())){
            this.df = df;
            return true;
        }
    }

    return false;
}

DocumentFile是一個把File類別再包裝的類別,詳細介紹可以參考官方文件

getContentResolver().getPersistedUriPermissions()

這一行是取出已有的權限,回傳一個UriPermission陣列

"sdcard1".equals(df.getName())

sdcard1是外接SDCard的固定名稱,所以如果有配對到表示有要求到權限

結論

雖然對於GOOGLE為什麼對外接SDCard做了限制而感到不解

但好險在5.0版本後有推出另類的解決方案,不然檔案管理的APP就GG了

花了一天試驗加找資料,寫篇文章記錄一下,希望可以幫到遇到相同問題的開發者們

Read more

[LeetCode] #12 Integer to Roman 解題

題目連結 題型解說 這是一題難度為普通的題目 需要設計一個方法,此方法會傳入一個整數 num 要求是把整數轉換成羅馬字母,轉換清單如下 I => 1 V => 5 X => 10 L => 50 C => 100 D => 500 M => 1000 但羅馬字母有一些特殊規則 4 並非 IIII 而是 IV,9 並非 VIIII 而是 IX 這規則同樣可以套用到 40 90 400 900 解題思路 既然知道特殊規則是一樣的,變得是使用的符號,那麼先從 num 取個位數開始 轉換完成後,把 num 除上 10,消除個位數,

By Michael

[LeetCode] #11 Container With Most Water 解題

題目連結 題型解說 這是一題難度為中等的題目 需要設計一個方法,此方法會傳入一個數字陣列 height 陣列中的元素代表每一個柱子的高度 現在需要計算出,該陣列中以某兩隻柱子為邊界,最多可以裝多少水 以範例來說 height = [1,8,6,2,5,4,8,3,7] 最多可以裝 7 * 7 = 49 單位的水 解題思路 計算面積就是底乘上高 底的計算方式為 「右邊柱子的 index」 減去 「左邊柱子的 index」 高就取最短的那一根柱子高度 拿題目給的例子來當範例 建立三個變數 result、left、right left、right 代表左右兩邊的 index result 代表目前最大容量,初始值 0 第一步,找出最短的柱子高度,

By Michael

[LeetCode] #941 Valid Mountain Array 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個數字陣列 arr 判斷陣列中的元素是不是由低到高再從高到低(山形)的排序,且不連續一個以上數字 比如說 [1,2,3,2] 就是一個山形陣列,但 [1,2,2,3,2] 不是,因為有兩個 2 [1,2,3,4,5] 和 [5,4,3,2,1] 也不算是山形陣列,前者只有往上沒有往下,後者相反 解題思路 準備一個數字變數(temp)和布林變數(asc),跑一次迴圈,有可能遇到如下狀況 1. 某個數字與前一個數字相同,這時候直接回傳 false

By Michael

[LeetCode] #944 Delete Columns to Make Sorted 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個字串陣列 strs 這個陣列中每個字串的長度都相同,字串內容都是小寫英文 需要檢查每個元素的第 N 個字元是不是由小至大排列,並回傳有幾個錯誤排列 比如傳入的陣列長這樣 ["cba","daf","ghi"] 取第一個字元 = cdg 取第二個字元 = bah 取第三個字元 = afi 其中第二組的結果(bah)並不是由小至大排列,故回傳 1 解題思路 這一題就用兩個迴圈各別把字元取出來,並比較是否比上一個字元大(Java 中的字元可以直接比較),如果不是就將結果+1 程式碼 Java class Solution { public int minDeletionSize(String[] strs) { int result = 0; for (int i = 0,

By Michael