2016-01-31 69 views
30

我目前正在構建一個文件管理應用程序,允許用戶瀏覽他們設備的文件系統。用戶從其設備的根目錄/開始,但可以瀏覽到他們想要的任何位置,例如內部閃存或SD卡。mkdir()在內部閃存中但不在SD卡中工作?

這個應用程序的關鍵要求之一是允許用戶在任何地方創建新的文件夾。像這樣的功能對於應用程序非常有用。但是,File#mkdir()方法在SD卡目錄中完全不起作用。

我向清單文件添加了適當的權限。我還寫了一個測試,以查看哪些目錄(我的Lollipop 5.0設備上存在的所有目錄)允許創建一個新文件夾。根據我的觀察,File#mkdir()僅在內部閃存存儲目錄內有效。

注意:請不要混淆Environment#getExternalStorageDirectory()與SD卡位置,如this article解釋。同樣在Lollipop 5.0上,我相信/storage/emulated/0//storage/sdcard0/指的是內部閃存,而/storage/emulated/1//storage/sdcard1/指的是SD卡(至少對於我正在測試的設備而言是如此)。

如何在非根用戶的Android設備上的外部存儲路徑之外的區域創建新的文件和文件夾?


清單:

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

測試:

... 
public class MainActivity extends AppCompatActivity { 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     setContentView(R.layout.activity_main); 

     final String NEW_FOLDER_NAME = "TestFolder"; 
     testPath(new File(Environment.getExternalStorageDirectory(), NEW_FOLDER_NAME)); 
     testPath(new File("/storage/emulated/0/", NEW_FOLDER_NAME)); 
     testPath(new File("/storage/emulated/1/", NEW_FOLDER_NAME)); 
     testPath(new File("/storage/sdcard0/Download/", NEW_FOLDER_NAME)); 
     testPath(new File("/storage/sdcard1/Pictures/", NEW_FOLDER_NAME)); 
    } 

    private void testPath(File path) { 
     String TAG = "Debug.MainActivity.java"; 
     String FOLDER_CREATION_SUCCESS = " mkdir() success: "; 

     boolean success = path.mkdir(); 
     Log.d(TAG, path.getAbsolutePath() + FOLDER_CREATION_SUCCESS + success); 
     path.delete(); 
    } 
} 

輸出:

/storage/emulated/0/TestFolder mkdir() success: true 
/storage/emulated/0/TestFolder mkdir() success: true 
/storage/emulated/1/TestFolder mkdir() success: false 
/storage/sdcard0/Download/TestFolder mkdir() success: true 
/storage/sdcard1/Pictures/TestFolder mkdir() success: false 
+0

你不能假設/存儲/ SD卡或/存儲/仿真地圖的任何東西。 OEM可以將這些重命名爲任何他們想要的東西。 –

+2

SD卡是安裝還是隻讀? – DominicEU

+0

@Gabe Sechan我知道這一點。我的應用程序實際上並沒有對SD卡和內置閃存驅動器的位置做任何假設,因爲它會加載'/ storage /'目錄,以便用戶可以選擇他們想要的安裝點。 – zxgear

回答

21

首先,你應該注意的是file.mkdir()file.mkdirs()返回false如果這個目錄已經存在。如果您想知道目錄是否存在,請使用(file.mkdir() || file.isDirectory())或者直接忽略返回值並致電file.isDirectory()(請參閱文檔)。

也就是說,你真正的問題是你需要權限才能在Android 5.0+上的可移動存儲上創建目錄。在Android上使用可移動SD卡非常可怕。

在Android 4.4(KitKat)上,Google限制訪問SD卡(請參閱here,herehere)。如果您需要在Android 4.4(KitKat)上的可移動SD卡上創建一個目錄,請參閱StackOverflow answer這導致此XDA post

在Android 5.0(Lollipop)上,Google推出了新的SD卡訪問API。有關示例用法,請參閱此stackoverflow answer

基本上,您需要使用DocumentFile#createDirectory(String displayName)來創建您的目錄。在創建該目錄之前,您需要先向用戶授予對您的應用程序的權限。


注:這是可移動存儲設備。如果您有權限android.permission.WRITE_EXTERNAL_STORAGE,使用File#mkdirs()將可用於內部存儲器(通常與Android上的外部存儲器混淆)。


我會後下面的一些示例代碼:

檢查是否需要徵得他的許可:

File sdcard = ... // the removable SD card 
List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions(); 
DocumentFile documentFile = null; 
boolean needPermissions = true; 

for (UriPermission permission : permissions) { 
    if (permission.isWritePermission()) { 
    documentFile = DocumentFile.fromTreeUri(context, permission.getUri()); 
    if (documentFile != null) { 
     if (documentFile.lastModified() == sdcard.lastModified()) { 
     needPermissions = false; 
     break; 
     } 
    } 
    } 
} 

下一頁(如果needPermissionstrue),則可以顯示一個對話框向用戶解釋他們需要選擇「SD卡」以使您的應用具有創建文件/目錄的權限,然後開始以下活動:

if (needPermissions) { 
    // show a dialog explaining that you need permission to create the directory 
    // here, we will just launch to chooser (what you need to do after showing the dialog) 
    startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), STORAGE_REQUEST_CODE); 
} else { 
    // we already have permission to write to the removable SD card 
    // use DocumentFile#createDirectory 
} 

現在,您將需要檢查resultCoderequestCodeonActivityResult

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
    if (requestCode == STORAGE_REQUEST_CODE && resultCode == RESULT_OK) { 
    File sdcard = ... // get the removable SD card 

    boolean needPermissions = true; 
    DocumentFile documentFile = DocumentFile.fromTreeUri(MainActivity.this, data.getData()); 
    if (documentFile != null) { 
     if (documentFile.lastModified() == sdcard.lastModified()) { 
     needPermissions = false; 
     } 
    } 

    if (needPermissions) { 
     // The user didn't select the "SD Card". 
     // You should try the process over again or do something else. 
    } else { 
     // remember this permission grant so we don't need to ask again. 
     getContentResolver().takePersistableUriPermission(data.getData(), 
      Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 
     // Now we can work with DocumentFile and create our directory 
     DocumentFile doc = DocumentFile.fromTreeUri(this, data.getData()); 
     // do stuff... 
    } 
    return; 
    } 
    super.onActivityResult(requestCode, resultCode, data); 
} 

這應該給你上DocumentFile和可移動SD卡在Android 5.0+工作的一個良好的開端。它可以是PITA。


此外,沒有公共API來獲取可移動SD卡的路徑(如果有的話)。你不應該依靠硬編碼"/storage/sdcard1"!在StackOverflow上有很多關於它的文章。許多解決方案使用環境變量SECONDARY_STORAGE。以下是可用於查找可移動存儲設備的兩種方法:

public static List<File> getRemovabeStorages(Context context) throws Exception { 
    List<File> storages = new ArrayList<>(); 

    Method getService = Class.forName("android.os.ServiceManager") 
     .getDeclaredMethod("getService", String.class); 
    if (!getService.isAccessible()) getService.setAccessible(true); 
    IBinder service = (IBinder) getService.invoke(null, "mount"); 

    Method asInterface = Class.forName("android.os.storage.IMountService$Stub") 
     .getDeclaredMethod("asInterface", IBinder.class); 
    if (!asInterface.isAccessible()) asInterface.setAccessible(true); 
    Object mountService = asInterface.invoke(null, service); 

    Object[] storageVolumes; 
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 
    String packageName = context.getPackageName(); 
    int uid = context.getPackageManager().getPackageInfo(packageName, 0).applicationInfo.uid; 
    Method getVolumeList = mountService.getClass().getDeclaredMethod(
     "getVolumeList", int.class, String.class, int.class); 
    if (!getVolumeList.isAccessible()) getVolumeList.setAccessible(true); 
    storageVolumes = (Object[]) getVolumeList.invoke(mountService, uid, packageName, 0); 
    } else { 
    Method getVolumeList = mountService.getClass().getDeclaredMethod("getVolumeList"); 
    if (!getVolumeList.isAccessible()) getVolumeList.setAccessible(true); 
    storageVolumes = (Object[]) getVolumeList.invoke(mountService, (Object[]) null); 
    } 

    for (Object storageVolume : storageVolumes) { 
    Class<?> cls = storageVolume.getClass(); 
    Method isRemovable = cls.getDeclaredMethod("isRemovable"); 
    if (!isRemovable.isAccessible()) isRemovable.setAccessible(true); 
    if ((boolean) isRemovable.invoke(storageVolume, (Object[]) null)) { 
     Method getState = cls.getDeclaredMethod("getState"); 
     if (!getState.isAccessible()) getState.setAccessible(true); 
     String state = (String) getState.invoke(storageVolume, (Object[]) null); 
     if (state.equals("mounted")) { 
     Method getPath = cls.getDeclaredMethod("getPath"); 
     if (!getPath.isAccessible()) getPath.setAccessible(true); 
     String path = (String) getPath.invoke(storageVolume, (Object[]) null); 
     storages.add(new File(path)); 
     } 
    } 
    } 

    return storages; 
} 

public static File getRemovabeStorageDir(Context context) { 
    try { 
    List<File> storages = getRemovabeStorages(context); 
    if (!storages.isEmpty()) { 
     return storages.get(0); 
    } 
    } catch (Exception ignored) { 
    } 
    final String SECONDARY_STORAGE = System.getenv("SECONDARY_STORAGE"); 
    if (SECONDARY_STORAGE != null) { 
    return new File(SECONDARY_STORAGE.split(":")[0]); 
    } 
    return null; 
} 
+0

謝謝你的幫助鏈接,我全部閱讀。由於所需的許可以遞歸方式工作(根據我的理解),是否也可以顯示一個對話框,明確要求許可使用「/」,而用戶不必指定路徑? – zxgear

+0

這個解決方案很混亂,但工作,我能夠在我的SD卡上創建一個新的文件夾。如果用戶不必被髮送到新的活動,如果可能的話,會更好。 – zxgear

+2

您應該只需要爲每個存儲設備請求一次權限。由於明顯的安全原因,它不適用於根目錄(「/」),但它應該用於任何可移動存儲。通過一些工作,你可以將邏輯從你的活動類中分離出來。但是,您仍然需要檢查'onActivityResult'。 –

2

path.mkdir()當目錄已經存在時也失敗。 您可以先添加一個檢查:

if (!path.exists()) { 
    boolean success = path.mkdir(); 
    Log.d(TAG, path.getAbsolutePath() + FOLDER_CREATION_SUCCESS + success); 
    path.delete(); 
} else { 
    Log.d(TAG, path.getAbsolutePath() + "already exists"); 
} 
1

試試這個。這對我來說可以。

final String NEW_FOLDER_NAME = "TestFolder"; 

String extStore = System.getenv("EXTERNAL_STORAGE"); 
File f_exts = new File(extStore, NEW_FOLDER_NAME); 

String secStore = System.getenv("SECONDARY_STORAGE"); 
File f_secs = new File(secStore, NEW_FOLDER_NAME); 

testPath(f_exts); 

textPath(f_secs); 

testPath功能改變布爾值如下

boolean success; 
if(path.exists()) { 
    // already created 
    success = true; 
} else { 
    success = path.mkdir(); 
} 

如果文件夾已經存在,path.mkdir()方法返回false。

並完成。

參考this問題。

2

對Kitkat谷歌限制訪問外部SD卡所以你不能寫入Kitkat外部存儲。

在棒棒糖谷歌作出了新的框架,以將數據寫入到外部存儲u必須使用新的DocumentFile

類,這是向後兼容的。

基本上U可以在應用程序到應用程序的根目錄中的OnStart請求許可,那麼在哪裏創建目錄