2016-01-31 13 views
30

Obecnie tworzę aplikację do zarządzania plikami, która pozwala użytkownikowi przeglądać system plików swojego urządzenia. Użytkownik zaczyna w katalogu głównym / swojego urządzenia, ale może przeglądać dowolne miejsce, takie jak wewnętrzna pamięć flash lub karta SD.mkdir() działa w wewnętrznej pamięci flash, ale nie na karcie SD?

Jednym z najważniejszych wymagań tej aplikacji jest umożliwienie użytkownikowi tworzenia nowych folderów w dowolnym miejscu. Taka funkcja byłaby niezwykle przydatna w przypadku aplikacji. Jednak metoda File#mkdir() nie działa w ogóle w katalogu kart SD.

Dodałem odpowiednie uprawnienia do pliku manifestu. Napisałem również test, aby zobaczyć, które katalogi (wszystkie istnieją na moim urządzeniu Lollipop 5.0) pozwalają na utworzenie nowego folderu. Z moich obserwacji wynika, że ​​File#mkdir() działa tylko wtedy, gdy znajduje się w wewnętrznym katalogu pamięci flash.

Uwaga: proszę nie mylić Environment#getExternalStorageDirectory() z umieszczeniem kart SD, jak wyjaśnił this article. Również na Lollipop 5.0, uważam, że /storage/emulated/0/ i /storage/sdcard0/ odnoszą się do wewnętrznej pamięci flash, podczas gdy /storage/emulated/1/ i /storage/sdcard1/ odnoszą się do karty SD (co najmniej jest prawdziwe dla urządzenia, z którym testuję).

Jak mogę utworzyć nowe pliki i foldery w obszarach poza ścieżką zewnętrznej pamięci masowej na niezrootowanych urządzeniach z Androidem?


Oczywisty:

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

Test:

... 
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(); 
    } 
} 

wyjściowa:

/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

Nie można zakładać, że/magazynowanie/sdcard lub/magazynowanie/emulowane mapa do niczego. OEM może zmienić nazwy tych elementów na dowolne. –

+2

Czy karta SD jest zamontowana lub tylko do odczytu? – DominicEU

+0

@Gabe Sechan Jestem tego świadomy. Moja aplikacja nie zawiera żadnych założeń, gdzie znajduje się karta SD i wewnętrzny dysk flash, ponieważ ładuje katalog '/ storage /', aby użytkownik mógł wybrać żądany punkt instalacji. – zxgear

Odpowiedz

21

Po pierwsze, należy pamiętać, że file.mkdir() i file.mkdirs() powraca false jeśli katalog już istnieje. Jeśli chcesz wiedzieć, czy katalog istnieje po powrocie, użyj opcji (file.mkdir() || file.isDirectory()) lub po prostu zignoruj ​​wartość zwracaną i zadzwoń pod numer file.isDirectory() (patrz dokumentacja).

Mimo to Twoim prawdziwym problemem jest to, że potrzebujesz uprawnień do utworzenia katalogu na wymiennym nośniku w systemie Android 5.0+. Praca z wymiennymi kartami SD na Androida jest przerażająca.

W systemie Android 4.4 (KitKat) Google ograniczył dostęp do kart SD (patrz here, here i here). Zobacz to StackOverflow answer, które prowadzi do tego XDA post, jeśli chcesz utworzyć katalog na wymiennej karcie SD w systemie Android 4.4 (KitKat).

W systemie Android 5.0 (Lollipop) firma Google wprowadziła nowe interfejsy dostępu do kart SD. Aby uzyskać przykładowe użycie, zapoznaj się z tym stackoverflow answer.

Zasadniczo do utworzenia katalogu należy użyć DocumentFile#createDirectory(String displayName). Przed utworzeniem tego katalogu musisz poprosić użytkownika o nadanie uprawnień do aplikacji.


UWAGA: Jest pamięci wymiennych. Używanie File#mkdirs() będzie działać na wewnętrznej pamięci masowej (która często jest mylona z zewnętrzną pamięcią masową w systemie Android), jeśli masz pozwolenie android.permission.WRITE_EXTERNAL_STORAGE.


będę pisać jakiś przykładowy kod poniżej:

Sprawdź, czy musisz poprosić o pozwolenie:

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; 
     } 
    } 
    } 
} 

Dalej (jeżeli needPermissions jest true), można wyświetlić okno dialogowe aby wyjaśnić użytkownikowi, że musi wybrać "kartę SD", aby dać Twojej aplikacji uprawnienia do tworzenia plików/katalogów, a następnie rozpocząć następującą aktywność:

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 
} 

Będziesz teraz trzeba sprawdzić resultCode i requestCode w onActivityResult:

@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); 
} 

To powinno dać dobry start na pracy z DocumentFile i wymiennych kartach SD na Androidzie 5.0+. Może to być PITA.


Ponadto, nie ma publicznego API, aby uzyskać ścieżkę do wymiennej karcie SD (jeśli w ogóle istnieje). Nie powinieneś polegać na hardcoding "/storage/sdcard1"! Istnieje sporo postów na ten temat w StackOverflow. Wiele rozwiązań wykorzystuje zmienną środowiskową SECONDARY_STORAGE. Poniżej przedstawiono dwie metody znalezienia wymiennych urządzeń magazynujących:

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

Dziękuję za pomocne linki, czytam je wszystkie . Ponieważ wymagane uprawnienie działa rekursywnie (z mojego zrozumienia), czy możliwe jest również wyświetlenie okna dialogowego, które jawnie prosi o pozwolenie na pracę z "/" bez konieczności podawania ścieżki przez użytkownika? – zxgear

+0

To rozwiązanie jest nieporządne, ale działa, udało mi się utworzyć nowy folder na mojej karcie SD. Byłoby lepiej, gdyby użytkownik nie musiał być wysyłany do nowego działania, jeśli jest to możliwe. – zxgear

+2

Powinieneś żądać pozwolenia tylko raz na urządzenie magazynujące. Nie będzie działać z głównym katalogiem ("/") z oczywistych względów bezpieczeństwa, ale powinien być użyty dla dowolnego wymiennego nośnika. Przy odrobinie pracy możesz oddzielić logikę od klasy aktywności. Jednak nadal będziesz musiał sprawdzić 'onActivityResult'. –

2

path.mkdir() ulega również awarii, gdy katalog już istnieje. Możesz dodać czek pierwszy:

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

Spróbuj z tym. Działa to dobrze dla mnie.

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); 

i zmienić wartość logiczną w testPath funkcji następująco

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

Jeśli katalog już istnieje, path.mkdir() metoda return false.

i gotowe. !!!

numer referencyjny od this pytanie.

2

na Google KitKat google ma ograniczony dostęp do zewnętrznego sdcard, więc nie będziesz mógł pisać do zewnętrznej pamięci w KitKat.

W Lollipop google wykonane nowe ramy do zapisu danych do pamięci zewnętrznej u mają do korzystania z nowego DocumentFile

klasę, która jest wstecznie kompatybilne.

Zasadniczo u może zwrócić się o pozwolenie na onStart z aplikacji do katalogu głównego aplikacji, a następnie u można utworzyć katalogu

Powiązane problemy