Google去年11月正式发布了Android 4.4,代号为KitKat(奇巧,雀巢的一款巧克力品牌),该系统带来了诸多新的特性。
但需要注意的是,该系统可能会让你之前一直正常使用的SD卡变为无用的“摆设”,因为根据新版本的API改进,应用程序将不能再往SD卡中写入文件。
来看Android开发者网站的“外部存储技术信息”文档中的描述:
引用WRITE_EXTERNAL_STORAGE只为设备上的主要外部存储授予写权限,应用程序无法将数据写入二级外部存储设备,除非指定了应用程序允许访问的特定的目录。
这目前只影响双存储设备,如果你的设备有内部存储空间,即通常所说的机身存储(这就是指主要外部存储),那么你的SD卡就是一个二级外部存储设备。
在Android 4.4中,如果你同时使用了机身存储和SD卡,那么应用程序将无法在SD卡中创建、修改、删除数据。比如,你无法使用文件管理器通过无线网络从电脑往SD卡中复制文件了。但是应用程序仍然可以往主存储的任意目录中写入数据,不受任何限制。
Google表示,这样做的目的是,通过这种方式进行限制,系统可以在应用程序被卸载后清除遗留文件。
目前三星已经通过OTA向部分手机发送了Android 4.4的更新,已经有Note3用户抱怨FX文件管理器现在不能往SD卡中复制内容了。
解决办法
获得系统的ROOT权限是一个解决方法。
很显然,这是针对用户的解决办法,但是并不是所有的用户都愿意进行ROOT,那么需要SD卡写入权限的开发者该如何做呢?
XDA论坛已经有大神给出了解决方案——在应用中嵌入一段代码,这段代码作用是在Android 4.4+设备上,如果其他方式写入失败,则将数据写入二级存储设备。
详细方案:<img class="BDE_Image" src="http://forum.xda-developers.com/showthread.php?p=50008987<br/><br/>Java代码<p>/*</p><p>*Copyright(C)2014NextApp,Inc.</p><p>*</p><p>*LicensedundertheApacheLicense,Version2.0(the"License");youmaynotusethisfileexceptincompliancewiththeLicense.</p><p>*YoumayobtainacopyoftheLicenseat</p><p>*</p><p>*http://www.apache.org/licenses/LICENSE-2.0</p><p>*</p><p>*Unlessrequiredbyapplicablelaworagreedtoinwriting,softwaredistributedundertheLicenseisdistributedonan"ASIS"</p><p>*BASIS,WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.SeetheLicenseforthespecificlanguage</p><p>*governingpermissionsandlimitationsundertheLicense.</p><p>*/</p><p></p><p>packagenextapp.mediafile;</p><p></p><p>importjava.io.File;</p><p>importjava.io.IOException;</p><p>importjava.io.OutputStream;</p><p></p><p>importandroid.content.ContentResolver;</p><p>importandroid.content.ContentValues;</p><p>importandroid.net.Uri;</p><p>importandroid.provider.MediaStore;</p><p></p><p>/**</p><p>*WrapperformanipulatingfilesviatheAndroidMediaContentProvider.AsofAndroid4.4KitKat,applicationscannolongerwrite</p><p>*tothe"secondarystorage"ofadevice.Writeoperationsusingthejava.io.FileAPIwillthusfail.Thisclassrestoresaccessto</p><p>*thosewriteoperationsbywayoftheMediaContentProvider.</p><p>*</p><p>*NotethatthisclassreliesontheinternaloperationalcharacteristicsofthemediacontentproviderAPI,andassuchisnot</p><p>*guaranteedtobefuture-proof.Thenagain,wedidallthinkthejava.io.FileAPIwasgoingtobefuture-proofformediacard</p><p>*access,soallbetsareoff.</p><p>*</p><p>*Ifyou'reforcedtousethisclass,it'sbecauseGoogle/AOSPmadeaverypoorAPIdecisioninAndroid4.4KitKat.</p><p>*Readmoreathttps://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn</p><p>*</p><p>*Yourapplicationmustdeclarethepermission"android.permission.WRITE_EXTERNAL_STORAGE".</p><p>*/</p><p>publicclassMediaFile{</p><p></p><p>privatefinalFilefile;</p><p>privatefinalContentResolvercontentResolver;</p><p>privatefinalUrifilesUri;</p><p>privatefinalUriimagesUri;</p><p></p><p>publicMediaFile(ContentResolvercontentResolver,Filefile){</p><p>this.file=file;</p><p>this.contentResolver=contentResolver;</p><p>filesUri=MediaStore.Files.getContentUri("external");</p><p>imagesUri=MediaStore.Images.Media.EXTERNAL_CONTENT_URI;</p><p>}</p><p></p><p>/**</p><p>*Deletesthefile.Returnstrueifthefilehasbeensuccessfullydeletedorotherwisedoesnotexist.Thisoperationisnot</p><p>*recursive.</p><p>*/</p><p>publicbooleandelete()</p><p>throwsIOException{</p><p>if(!file.exists()){</p><p>returntrue;</p><p>}</p><p></p><p>booleandirectory=file.isDirectory();</p><p>if(directory){</p><p>//Verifydirectorydoesnotcontainanyfiles/directorieswithinit.</p><p>String[]files=file.list();</p><p>if(files!=null&&files.length>0){</p><p>returnfalse;</p><p>}</p><p>}</p><p></p><p>Stringwhere=MediaStore.MediaColumns.DATA+"=?";</p><p>String[]selectionArgs=newString[]{file.getAbsolutePath()};</p><p></p><p>//Deletetheentryfromthemediadatabase.Thiswillactuallydeletemediafiles(images,audio,andvideo).</p><p>contentResolver.delete(filesUri,where,selectionArgs);</p><p></p><p>if(file.exists()){</p><p>//Ifthefileisnotamediafile,createanewentrysuggestingthatthislocationisanimage,even</p><p>//thoughitisnot.</p><p>ContentValuesvalues=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath());</p><p>contentResolver.insert(imagesUri,values);</p><p></p><p>//Deletethecreatedentry,suchthatcontentproviderwilldeletethefile.</p><p>contentResolver.delete(filesUri,where,selectionArgs);</p><p>}</p><p></p><p>return!file.exists();</p><p>}</p><p></p><p>publicFilegetFile(){</p><p>returnfile;</p><p>}</p><p></p><p>/**</p><p>*Createsanewdirectory.Returnstrueifthedirectorywassuccessfullycreatedorexists.</p><p>*/</p><p>publicbooleanmkdir()</p><p>throwsIOException{</p><p>if(file.exists()){</p><p>returnfile.isDirectory();</p><p>}</p><p></p><p>ContentValuesvalues;</p><p>Uriuri;</p><p></p><p>//Createamediadatabaseentryforthedirectory.Thisstepwillnotactuallycausethedirectorytobecreated.</p><p>values=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath());</p><p>contentResolver.insert(filesUri,values);</p><p></p><p>//Createanentryforatemporaryimagefilewithinthecreateddirectory.</p><p>//Thisstepactuallycausesthecreationofthedirectory.</p><p>values=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath()+"/temp.jpg" unselectable="on" pic_type="1"/>
");
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);
}
}
但需要注意的是,该系统可能会让你之前一直正常使用的SD卡变为无用的“摆设”,因为根据新版本的API改进,应用程序将不能再往SD卡中写入文件。
来看Android开发者网站的“外部存储技术信息”文档中的描述:
引用WRITE_EXTERNAL_STORAGE只为设备上的主要外部存储授予写权限,应用程序无法将数据写入二级外部存储设备,除非指定了应用程序允许访问的特定的目录。
这目前只影响双存储设备,如果你的设备有内部存储空间,即通常所说的机身存储(这就是指主要外部存储),那么你的SD卡就是一个二级外部存储设备。
在Android 4.4中,如果你同时使用了机身存储和SD卡,那么应用程序将无法在SD卡中创建、修改、删除数据。比如,你无法使用文件管理器通过无线网络从电脑往SD卡中复制文件了。但是应用程序仍然可以往主存储的任意目录中写入数据,不受任何限制。
Google表示,这样做的目的是,通过这种方式进行限制,系统可以在应用程序被卸载后清除遗留文件。
目前三星已经通过OTA向部分手机发送了Android 4.4的更新,已经有Note3用户抱怨FX文件管理器现在不能往SD卡中复制内容了。
解决办法
获得系统的ROOT权限是一个解决方法。
很显然,这是针对用户的解决办法,但是并不是所有的用户都愿意进行ROOT,那么需要SD卡写入权限的开发者该如何做呢?
XDA论坛已经有大神给出了解决方案——在应用中嵌入一段代码,这段代码作用是在Android 4.4+设备上,如果其他方式写入失败,则将数据写入二级存储设备。
详细方案:<img class="BDE_Image" src="http://forum.xda-developers.com/showthread.php?p=50008987<br/><br/>Java代码<p>/*</p><p>*Copyright(C)2014NextApp,Inc.</p><p>*</p><p>*LicensedundertheApacheLicense,Version2.0(the"License");youmaynotusethisfileexceptincompliancewiththeLicense.</p><p>*YoumayobtainacopyoftheLicenseat</p><p>*</p><p>*http://www.apache.org/licenses/LICENSE-2.0</p><p>*</p><p>*Unlessrequiredbyapplicablelaworagreedtoinwriting,softwaredistributedundertheLicenseisdistributedonan"ASIS"</p><p>*BASIS,WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.SeetheLicenseforthespecificlanguage</p><p>*governingpermissionsandlimitationsundertheLicense.</p><p>*/</p><p></p><p>packagenextapp.mediafile;</p><p></p><p>importjava.io.File;</p><p>importjava.io.IOException;</p><p>importjava.io.OutputStream;</p><p></p><p>importandroid.content.ContentResolver;</p><p>importandroid.content.ContentValues;</p><p>importandroid.net.Uri;</p><p>importandroid.provider.MediaStore;</p><p></p><p>/**</p><p>*WrapperformanipulatingfilesviatheAndroidMediaContentProvider.AsofAndroid4.4KitKat,applicationscannolongerwrite</p><p>*tothe"secondarystorage"ofadevice.Writeoperationsusingthejava.io.FileAPIwillthusfail.Thisclassrestoresaccessto</p><p>*thosewriteoperationsbywayoftheMediaContentProvider.</p><p>*</p><p>*NotethatthisclassreliesontheinternaloperationalcharacteristicsofthemediacontentproviderAPI,andassuchisnot</p><p>*guaranteedtobefuture-proof.Thenagain,wedidallthinkthejava.io.FileAPIwasgoingtobefuture-proofformediacard</p><p>*access,soallbetsareoff.</p><p>*</p><p>*Ifyou'reforcedtousethisclass,it'sbecauseGoogle/AOSPmadeaverypoorAPIdecisioninAndroid4.4KitKat.</p><p>*Readmoreathttps://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn</p><p>*</p><p>*Yourapplicationmustdeclarethepermission"android.permission.WRITE_EXTERNAL_STORAGE".</p><p>*/</p><p>publicclassMediaFile{</p><p></p><p>privatefinalFilefile;</p><p>privatefinalContentResolvercontentResolver;</p><p>privatefinalUrifilesUri;</p><p>privatefinalUriimagesUri;</p><p></p><p>publicMediaFile(ContentResolvercontentResolver,Filefile){</p><p>this.file=file;</p><p>this.contentResolver=contentResolver;</p><p>filesUri=MediaStore.Files.getContentUri("external");</p><p>imagesUri=MediaStore.Images.Media.EXTERNAL_CONTENT_URI;</p><p>}</p><p></p><p>/**</p><p>*Deletesthefile.Returnstrueifthefilehasbeensuccessfullydeletedorotherwisedoesnotexist.Thisoperationisnot</p><p>*recursive.</p><p>*/</p><p>publicbooleandelete()</p><p>throwsIOException{</p><p>if(!file.exists()){</p><p>returntrue;</p><p>}</p><p></p><p>booleandirectory=file.isDirectory();</p><p>if(directory){</p><p>//Verifydirectorydoesnotcontainanyfiles/directorieswithinit.</p><p>String[]files=file.list();</p><p>if(files!=null&&files.length>0){</p><p>returnfalse;</p><p>}</p><p>}</p><p></p><p>Stringwhere=MediaStore.MediaColumns.DATA+"=?";</p><p>String[]selectionArgs=newString[]{file.getAbsolutePath()};</p><p></p><p>//Deletetheentryfromthemediadatabase.Thiswillactuallydeletemediafiles(images,audio,andvideo).</p><p>contentResolver.delete(filesUri,where,selectionArgs);</p><p></p><p>if(file.exists()){</p><p>//Ifthefileisnotamediafile,createanewentrysuggestingthatthislocationisanimage,even</p><p>//thoughitisnot.</p><p>ContentValuesvalues=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath());</p><p>contentResolver.insert(imagesUri,values);</p><p></p><p>//Deletethecreatedentry,suchthatcontentproviderwilldeletethefile.</p><p>contentResolver.delete(filesUri,where,selectionArgs);</p><p>}</p><p></p><p>return!file.exists();</p><p>}</p><p></p><p>publicFilegetFile(){</p><p>returnfile;</p><p>}</p><p></p><p>/**</p><p>*Createsanewdirectory.Returnstrueifthedirectorywassuccessfullycreatedorexists.</p><p>*/</p><p>publicbooleanmkdir()</p><p>throwsIOException{</p><p>if(file.exists()){</p><p>returnfile.isDirectory();</p><p>}</p><p></p><p>ContentValuesvalues;</p><p>Uriuri;</p><p></p><p>//Createamediadatabaseentryforthedirectory.Thisstepwillnotactuallycausethedirectorytobecreated.</p><p>values=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath());</p><p>contentResolver.insert(filesUri,values);</p><p></p><p>//Createanentryforatemporaryimagefilewithinthecreateddirectory.</p><p>//Thisstepactuallycausesthecreationofthedirectory.</p><p>values=newContentValues();</p><p>values.put(MediaStore.Files.FileColumns.DATA,file.getAbsolutePath()+"/temp.jpg" unselectable="on" pic_type="1"/>
");
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);
}
}
