以前,Android 开发者习惯在根目录建一个自己应用的文件夹,用于存放应用的数据。这样会导致用户卸载后,应用数据不会随之删除。导致手机文件特别混乱,长期占用空间,而且容易泄露用户隐私。
其实 Android 早就提供了 getCacheDir()、getFilesDir()、getExternalFilesDir()、getExternalCacheDir() 等 API 供开发者使用,但是开发者为了方便,没有去用。
为了解决这个问题,从 Android 10 开始,Google 添加了一个新特性 Scoped Storage,我们称之为分区存储,也可以称为沙盒。
在 Android 10 上,仍然可以通过以下两种手段避开分区存储:
在 Android 11 上,requestLegacyExternalStorage 会失效,没有效果。但是又增加了 preserveLegacyExternalStorage 属性,对于覆盖安装的应用还能继续用,但是新应用不能用。
至于 targetSdkVersion,上传到 Google Play 的应用,Google 要求必须设成 30 及以上。
沙盒目录
通过 getExternalFilesDir() 等获取到的目录,随着 App 卸载会被删除。
不过可以在 manifest 中设置 android:hasFragileUserData="true" 让用户选择是否删除。
公共目录
DCIM、Photos、Images、Videos、Audio、Downloads 等目录, App 卸载后会保留。
重点说下公共目录,沙盒目录就不详细介绍了,沙盒目录可以通过系统提供的接口直接获取,可以直接通过路径读写,也不需要定义任何读写权限,很简单。
访问公共目录需要通过 MediaStore 或者 Storage Access Framework(以下简称 SAF)。媒体文件(图片,音频,视频)能通过 MediaStore 和 SAF 两种方式访问,非媒体文件只能通过 SAF 访问。
关于 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 读写权限,MediaStore 访问应用自身存放到公共目录下的文件不需要申请权限(但是如果应用卸载后重装,之前保存的文件将不属于本应用创建的文件),而如果要访问其他应用保存到公共目录下的文件则需要申请权限。
MediaStore 通过 Uri 操作文件。
各个目录的 Uri 如下:
| 类型 | Uri | Uri 常量 | 默认路径 |
|---|---|---|---|
| Image | content://media/external/images/media | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | Pictures |
| Video | content://media/external/video/media | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | Movies |
| Audio | content://media/external/audio/media | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | Music |
| Download | content://media/external/downloads | MediaStore.Downloads.EXTERNAL_CONTENT_URI | Download |
| File | content://media/external/ | MediaStore.Files.getContentUri(“external”) | Documents |
// 从 Assets 读取 Bitmap
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(getAssets().open("test.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null) return;
// 获取保存文件的 Uri
ContentResolver contentResolver = getContentResolver();
ContentValues values = new ContentValues();
Uri insertUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
// 保存图片到 Pictures 目录下
if (insertUri != null) {
OutputStream os = null;
try {
os = contentResolver.openOutputStream(insertUri);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的例子直接把图片保存到 Pictures 根目录,如果要在 Pictures 下创建子目录,需要用到 RELATIVE_PATH(Android 版本 >= 10)。
修改上面的例子,把子目录添加进 ContentValues:
ContentValues values = new ContentValues();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 指定子目录,否则保存到对应媒体类型文件夹根目录
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES +"/test");
}
还可以向 ContentValues 中添加其他信息,如:文件名,MIME 等
继续修改上面的例子:
ContentValues values = new ContentValues();
// 获取保存文件的 Uri
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
// 指定保存的文件名,如果不设置,则系统会取当前的时间戳作为文件名
values.put(MediaStore.Images.Media.DISPLAY_NAME, "test_" + System.currentTimeMillis() + ".png");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 指定子目录,否则保存到对应媒体类型文件夹根目录
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/test");
}
获取到对应的 Uri 之后 contentResolver.delete(uri,null,null) 即可。
// 查询
ContentResolver contentResolver = getContentResolver();
Cursor cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{
MediaStore.Images.Media._ID,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT
},
MediaStore.Images.Media._ID + " > ? ", new String[]{"100"},
MediaStore.Images.Media._ID + " DESC"
);
// 得到所有的 Uri
List<Uri> filesUris = new ArrayList<>();
while (cursor.moveToNext()) {
int index = cursor.getColumnIndex(MediaStore.Images.Media._ID);
Uri uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(index)
);
filesUris.add(uri);
}
cursor.close();
// 通过 Uri 获取具体内容并显示到界面上
ParcelFileDescriptor pfd = null;
try {
pfd = contentResolver.openFileDescriptor(filesUris.get(0), "r");
if (pfd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
((ImageView) findViewById(R.id.image)).setImageBitmap(bitmap);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (pfd != null) {
try {
pfd.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如上文所诉,访问自己应用创建的文件不需要 READ_EXTERNAL_STORAGE 权限。以上代码获取到的 filesUris 只包含本应用之前创建的文件。
如果需要连其他应用的文件一起获取,则申请下 READ_EXTERNAL_STORAGE 权限即可。
同理,需要申请 WRITE_EXTERNAL_STORAGE 权限。
但是,即便申请了 WRITE_EXTERNAL_STORAGE 权限之后,还是会报如下异常:
android.app.RecoverableSecurityException: xxx has no access to content://media/external/images/media/100
这是因为还需要向用户申请修改的权限。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
delete();
} catch (RecoverableSecurityException e) {
e.printStackTrace();
// 弹出对话框,向用户申请修改其他应用文件的权限
requestConfirmDialog(e);
}
}
private void delete() {
Uri uri = Uri.parse("content://media/external/images/media/100");
getContentResolver().delete(uri, null, null);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private void requestConfirmDialog(RecoverableSecurityException e) {
try {
startIntentSenderForResult(
e.getUserAction().getActionIntent().getIntentSender()
, 0, null, 0, 0, 0, null);
} catch (IntentSender.SendIntentException ex) {
ex.printStackTrace();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK){
delete();
}
}
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
private void downloadApkAndInstall(String downloadUrl, String apkName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// 使用原始方式
} else {
new Thread(() -> {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
URL url = new URL(downloadUrl);
URLConnection urlConnection = url.openConnection();
InputStream is = urlConnection.getInputStream();
bis = new BufferedInputStream(is);
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, apkName);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
ContentResolver contentResolver = getContentResolver();
Uri uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
OutputStream os = contentResolver.openOutputStream(uri);
bos = new BufferedOutputStream(os);
byte[] buffer = new byte[1024];
int bytes = bis.read(buffer);
while (bytes >= 0) {
bos.write(buffer, 0, bytes);
bos.flush();
bytes = bis.read(buffer);
}
runOnUiThread(() -> installAPK(uri));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bos != null) bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
private void installAPK(Uri uri) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
}
SAF 在 Android 4.4 就支持了。
SAF 通过系统提供的标准化 UI 浏览和修改手机中的文件,如下图

private void createFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_TITLE, "test_create.png");
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null || resultCode != RESULT_OK) return;
if (requestCode == WRITE_REQUEST_CODE) {
Log.d("tianjf", "write uri : " + data.getData());
}
}
运行之后,会启动标准文件管理器 UI 保存文件。
写文件不需要申请写权限。
因为有可能读取其他应用创建的文件,所以需要申请读权限。
protected void readFiles() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null || resultCode != RESULT_OK) return;
if (requestCode == READ_REQUEST_CODE) {
Log.d("tianjf", "read uri : " + data.getData());
process(data.getData());
}
}
private void process(Uri uri) {
String[] selectionArgs = new String[]{DocumentsContract.getDocumentId(uri).split(":")[1]};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null, MediaStore.Images.Media._ID + "=?",
selectionArgs, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
if (index > -1) {
String path = cursor.getString(index);
Log.d("tianjf", "onActivityResult path=" + path + ";id=" + selectionArgs[0]);
}
}
cursor.close();
}
}
protected void readFolder() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, READ_FOLDER_REQUEST_CODE);
}
// 选取文件夹然后在文件夹中创建子文件夹和文件
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null || resultCode != RESULT_OK) return;
if (requestCode == READ_FOLDER_REQUEST_CODE) {
Log.d("tianjf", "read folder uri : " + data.getData());
DocumentFile selectedFolder = DocumentFile.fromTreeUri(this, data.getData());
DocumentFile newFolder = selectedFolder.createDirectory("newFolder");
DocumentFile newFile = newFolder.createFile("text/plain", "test.txt");
try {
getContentResolver().openOutputStream(newFile.getUri()).write("Hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
rpartition和partition有什么区别?我已经阅读了文档,但我认为它们是一样的。只是那些出现在后来的ruby版本中吗? 最佳答案 以下示例将有助于识别差异:"abccba".partition("b")#=>["a","b","ccba"]"abccba".rpartition("b")#=>["abcc","b","a"]所以区别在于rpartition搜索最右边的匹配项,而不是最左边的匹配项。 关于Rubyrpartition与分区?,我们在StackOverflow
我正在编写一个简单的静态Rack应用程序。查看下面的config.ru代码:useRack::Static,:urls=>["/elements","/img","/pages","/users","/css","/js"],:root=>"archive"map'/'dorunProc.new{|env|[200,{'Content-Type'=>'text/html','Cache-Control'=>'public,max-age=6400'},File.open('archive/splash.html',File::RDONLY)]}endmap'/pages/search.
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我去了这个website查看Rails5.0.0和Rails5.1.1之间的区别为什么5.1.1不再包含:config/initializers/session_store.rb?谢谢 最佳答案 这是删除它的提交:Setupdefaultsessionstoreinternally,nolongerthroughanapplicationinitializer总而言之,新应用没有该初始化器,session存储默认设置为cookie存储。即与在该初始值设定项的生成版本中指定的值相同。 关于
我刚刚安装了带有RVM的Ruby2.2.0,并尝试使用它得到了这个:$rvmuse2.2.0--defaultUsing/Users/brandon/.rvm/gems/ruby-2.2.0dyld:Librarynotloaded:/usr/local/lib/libgmp.10.dylibReferencedfrom:/Users/brandon/.rvm/rubies/ruby-2.2.0/bin/rubyReason:Incompatiblelibraryversion:rubyrequiresversion13.0.0orlater,butlibgmp.10.dylibpro
我正在关注Hartl的railstutorial.org并已到达11.4.4:Imageuploadinproduction.我做了什么:注册亚马逊网络服务在AmazonIdentityandAccessManagement中,我创建了一个用户。用户创建成功。在AmazonS3中,我创建了一个新存储桶。设置新存储桶的权限:权限:本教程指示“授予上一步创建的用户读写权限”。但是,在存储桶的“权限”下,未提及新用户名。我只能在每个人、经过身份验证的用户、日志传送、我和亚马逊似乎根据我的名字+数字创建的用户名之间进行选择。我已经通过选择经过身份验证的用户并选中了上传/删除和查看权限的框(而不
我正在使用mechanize登录网站,然后检索页面。我遇到了一些问题,我怀疑这是由于cookie中的某些值造成的。当Mechanize登录网站时,我假设它存储了cookie。如何通过Mechanize打印出存储在cookie中的所有数据? 最佳答案 代理有一个cookie方法。agent=Mechanize.newpage=agent.get("http://www.google.com/")agent.cookiesagent.cookies.to_scookie返回一个Mechanize::Cookiesobject
我以为它们存储在cookie中-但不,检查cookie没有任何结果。session也不存储它们。那么,我在哪里可以找到它们?我需要这个来直接设置它们(而不是通过flashhash)。 最佳答案 它们存储在inyoursessionstore.自rails2.0以来的默认设置是cookie存储,但请检查config/initializers/session_store.rb以检查您是否使用默认设置以外的东西。 关于ruby-on-rails-闪存消息存储在哪里?,我们在StackOverf
我正在运行Ubuntu11.10并像这样安装Ruby1.9:$sudoapt-getinstallruby1.9rubygems一切都运行良好,但ri似乎有空文档。ri告诉我文档是空的,我必须安装它们。我执行此操作是因为我读到它会有所帮助:$rdoc--all--ri现在,当我尝试打开任何文档时:$riArrayNothingknownaboutArray我搜索的其他所有内容都是一样的。 最佳答案 这个呢?apt-getinstallri1.8编辑或者试试这个:(非rvm)geminstallrdocrdoc-datardoc-da