但需要注意的是,该系统可能会让你之前一直正常使用的SD卡变为无用的“摆设”,因为 根据新版本的API改进,应用程序将不能再往SD卡中写入文件。
来看Android开发者网站的 “外部存储技术信息”文档 中的描述:
这目前只影响双存储设备, 如果你的设备有内部存储空间,即通常所说的机身存储(这就是指主要外部存储),那么你的SD卡就是一个二级外部存储设备。
在Android 4.4中,如果你同时使用了机身存储和SD卡,那么应用程序将无法在SD卡中创建、修改、删除数据。比如,你无法使用文件管理器通过无线网络从电脑往SD卡中复制文件了。但是应用程序仍然可以往主存储的任意目录中写入数据,不受任何限制。
Google表示, 这样做的目的是,通过这种方式进行限制,系统可以在应用程序被卸载后清除遗留文件。
目前三星已经通过OTA向部分手机发送了Android 4.4的更新,已经有Note3用户抱怨FX文件管理器现在不能往SD卡中复制内容了。
解决办法
获得系统的ROOT权限是一个解决方法。
很显然,这是针对用户的解决办法,但是并不是所有的用户都愿意进行ROOT,那么需要SD卡写入权限的开发者该如何做呢?
XDA论坛已经有大神给出了解决方案——在应用中嵌入一段代码 ,这段代码作用是在Android 4.4+设备上,如果其他方式写入失败,则将数据写入二级存储设备。
详细方案: http://forum.xda-developers.com/showthread.php?p=50008987
- /*
- * Copyright (C) 2014 NextApp, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
- * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- */
- package nextapp.mediafile;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import android.content.ContentResolver;
- import android.content.ContentValues;
- import android.net.Uri;
- import android.provider.MediaStore;
- /**
- * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
- * to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
- * those write operations by way of the Media Content Provider.
- *
- * Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
- * guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
- * access, so all bets are off.
- *
- * If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
- * Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
- *
- * Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
- */
- public class MediaFile {
- private final File file;
- private final ContentResolver contentResolver;
- private final Uri filesUri;
- private final Uri imagesUri;
- public MediaFile(ContentResolver contentResolver, File file) {
- this.file = file;
- this.contentResolver = contentResolver;
- filesUri = MediaStore.Files.getContentUri("external");
- imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
- }
- /**
- * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
- * recursive.
- */
- public boolean delete()
- throws IOException {
- if (!file.exists()) {
- return true;
- }
- boolean directory = file.isDirectory();
- if (directory) {
- // Verify directory does not contain any files/directories within it.
- String[] files = file.list();
- if (files != null && files.length > 0) {
- return false;
- }
- }
- String where = MediaStore.MediaColumns.DATA + "=?";
- String[] selectionArgs = new String[] { file.getAbsolutePath() };
- // Delete the entry from the media database. This will actually delete media files (images, audio, and video).
- contentResolver.delete(filesUri, where, selectionArgs);
- if (file.exists()) {
- // If the file is not a media file, create a new entry suggesting that this location is an image, even
- // though it is not.
- ContentValues values = new ContentValues();
- values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
- contentResolver.insert(imagesUri, values);
- // Delete the created entry, such that content provider will delete the file.
- contentResolver.delete(filesUri, where, selectionArgs);
- }
- return !file.exists();
- }
- public File getFile() {
- return file;
- }
- /**
- * Creates a new directory. Returns true if the directory was successfully created or exists.
- */
- public boolean mkdir()
- throws IOException {
- if (file.exists()) {
- return file.isDirectory();
- }
- ContentValues values;
- Uri uri;
- // Create a media database entry for the directory. This step will not actually cause the directory to be created.
- values = new ContentValues();
- values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
- contentResolver.insert(filesUri, values);
- // Create an entry for a temporary image file within the created directory.
- // This step actually causes the creation of the directory.
- values = new ContentValues();
- values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath() + "/temp.jpg");
- uri = contentResolver.insert(imagesUri, values);
- // Delete the temporary entry.
- contentResolver.delete(uri, null, null);
- return file.exists();
- }
- /**
- * Returns an OutputStream to write to the file. The file will be truncated immediately.
- */
- public OutputStream write()
- throws IOException {
- if (file.exists() && file.isDirectory()) {
- throw new IOException("File exists and is a directory.");
- }
- // Delete any existing entry from the media database.
- // This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
- String where = MediaStore.MediaColumns.DATA + "=?";
- String[] selectionArgs = new String[] { file.getAbsolutePath() };
- contentResolver.delete(filesUri, where, selectionArgs);
- ContentValues values = new ContentValues();
- values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
- Uri uri = contentResolver.insert(filesUri, values);
- if (uri == null) {
- // Should not occur.
- throw new IOException("Internal error.");
- }
- return contentResolver.openOutputStream(uri);
- }
- }
In Android 4.4 KitKat, Google/AOSP made the following change to the API specification, much to the detriment of app developers and users:
"The WRITE_EXTERNAL_STORAGE permission must only grant write access to the primary external storage on a device. Apps must not be allowed to write to secondary external storage devices, except in their package-specific directories as allowed by synthesized permissions." |
You can read my rather unhappy write-up about it here: https://plus.google.com/108338299717...ts/gjnmuaDM8sn
This only applies to dual-storage devices, i.e., devices with a user-writable internal flash storage AND a removable SD card. But if your device has both, as of Android 4.4, apps will no longer be able to write arbitrarily to the "secondary" storage (the SD card).
There is however still an API exposed that will allow you to write to secondary storage, notably the media content provider. This is far from an ideal solution, and I imagine that someday it will not be possible.
I've written a tiny bit of code to let your applications continue to work with files on the SD card using the media content provider. This code should only be used to write to the secondary storage on Android 4.4+ devices if all else fails. I would strongly recommend that you NEVER rely on this code. This code DOES NOT use root access.
The class:
/*
* Copyright (C) 2014 NextApp, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package nextapp.mediafile;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.provider.MediaStore;
/**
* Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
* to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
* those write operations by way of the Media Content Provider.
*
* Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
* guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
* access, so all bets are off.
*
* If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
* Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
*
* Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
*/
public class MediaFile {
private final File file;
private final ContentResolver contentResolver;
private final Uri filesUri;
private final Uri imagesUri;
public MediaFile(ContentResolver contentResolver, File file) {
this.file = file;
this.contentResolver = contentResolver;
filesUri = MediaStore.Files.getContentUri("external");
imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
/**
* Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
* recursive.
*/
public boolean delete()
throws IOException {
if (!file.exists()) {
return true;
}
boolean directory = file.isDirectory();
if (directory) {
// Verify directory does not contain any files/directories within it.
String[] files = file.list();
if (files != null && files.length > 0) {
return false;
}
}
String where = MediaStore.MediaColumns.DATA + "=?";
String[] selectionArgs = new String[] { file.getAbsolutePath() };
// Delete the entry from the media database. This will actually delete media files (images, audio, and video).
contentResolver.delete(filesUri, where, selectionArgs);
if (file.exists()) {
// If the file is not a media file, create a new entry suggesting that this location is an image, even
// though it is not.
ContentValues values = new ContentValues();
values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
contentResolver.insert(imagesUri, values);
// Delete the created entry, such that content provider will delete the file.
contentResolver.delete(filesUri, where, selectionArgs);
}
return !file.exists();
}
public File getFile() {
return file;
}
/**
* Creates a new directory. Returns true if the directory was successfully created or exists.
*/
public boolean mkdir()
throws IOException {
if (file.exists()) {
return file.isDirectory();
}
ContentValues values;
Uri uri;
// Create a media database entry for the directory. This step will not actually cause the directory to be created.
values = new ContentValues();
values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
contentResolver.insert(filesUri, values);
// Create an entry for a temporary image file within the created directory.
// This step actually causes the creation of the directory.
values = new ContentValues();
values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath() + "/temp.jpg");
uri = contentResolver.insert(imagesUri, values);
// Delete the temporary entry.
contentResolver.delete(uri, null, null);
return file.exists();
}
/**
* Returns an OutputStream to write to the file. The file will be truncated immediately.
*/
public OutputStream write()
throws IOException {
if (file.exists() && file.isDirectory()) {
throw new IOException("File exists and is a directory.");
}
// Delete any existing entry from the media database.
// This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
String where = MediaStore.MediaColumns.DATA + "=?";
String[] selectionArgs = new String[] { file.getAbsolutePath() };
contentResolver.delete(filesUri, where, selectionArgs);
ContentValues values = new ContentValues();
values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
Uri uri = contentResolver.insert(filesUri, values);
if (uri == null) {
// Should not occur.
throw new IOException("Internal error.");
}
return contentResolver.openOutputStream(uri);
}
}
Eclipse project with test app: http://android.nextapp.com/content/m...MediaWrite.zip
APK of test app: http://android.nextapp.com/content/m...MediaWrite.apk
The test project is currently configured to target the path /storage/extSdCard/MediaWriteTest (this is correct for a Note3, at least on 4.3...make sure you don't have anything there). Edit MainActivity.java in the Eclipse project to change it.
And again, let me stress that the above code might not work in the future should Google dislike it. I wouldn't recommend that the average app developer make use of this code, but if you're writing a file manager (or something else that competes with any of my other apps) , it might be useful to you. And actually at the time of writing, this functionality is NOT in FX File Explorer or WebSharing.
1.对多个sd卡支持
从4.4开始android已经支持多了sd卡(之前由厂商自己实现)
可通过以下方法获取
Context.getExternalFilesDirs(), 返回多个sd卡的该应用私有数据区的files目录
/storage/sdcard0/Android/data/<包名>/files /storage/sdcard1/Android/data/<包名>/files |
Context.getExternalCacheDirs(), 返回多个sd卡下该应用私有数据库的缓存目录
/storage/sdcard0/Android/data/<包名>/caches /storage/sdcard1/Android/data/<包名>/caches |
Context.getObbDirs(), 返回多个sd卡下obb目录下的私有数据
/storage/sdcard0/Android/obb/<包名> /storage/sdcard1/Android/obb/<包名> |
目前这些api均为hide,需通过反射调用
2.对读写权限的修改
如果应该仅需读取sd卡下该应用私有数据的数据,则不需要申请读写权限(WRITE_EXTERNAL_STORAGE 、READ_EXTERNAL_STORAGE )
如果需要读取sd卡其他目录,则需声明读sd卡权限
如果需要写入sd卡其他目录,则需声明写sd卡权限
对于存在多个sd卡的情况,仅对主卡、及每张卡的私有数据区下的文件有读写权限,对其他卡没有写权限(之前厂商实现中,对每张sd卡均有读写权限)