preface

Storage adaptation series articles:

Android- Storage Basics Android-10, 11- Storage fully adapted (top) Android-10, 11- Storage fully adapted (bottom) Android-FileProvider- Easy to master

This article will focus on how to adapt to Android 10.0 and 11.0. Through this article, you will learn:

1. Basic knowledge of MediaStore 2. Read and write files through Uri 3

1. Basic knowledge of MediaStore

To review storage area partitioning again:

As we concluded in the previous article, the storage access changes in Android 10.0 include:

Own external storage – Shared storage space and Own external storage – Other directories

In these two places, the file cannot be accessed directly through a path, but rather through a Uri.

Shared Storage Space

The shared storage space stores files such as pictures, videos and audio files. These resources are public and all apps can access them.

The system has the external.db database, which has the files table, which stores a lot of information about shared files, such as width and height, latitude and longitude, storage path of pictures, width and height, length and storage path of videos, etc. The real place for files is in shared storage.

1. Save pictures to album When App1 saves pictures to album, the simple process is as follows:

1. Write path information into database and obtain Uri 2. Construct output stream by Uri 3

When App2 gets an image from an album, the simple process is as follows:

Create Cursor from Cursor; create Uri; create Cursor from Uri

The above simple analysis of file writing and reading in shared storage space with pictures as an example, in fact, the same is true for video and audio steps.

MediaStore role

There are pictures, videos, audio and downloaded files in the shared storage space. How can the App distinguish these types when it obtains or inserts files?

This is where MediaStore is needed. Take a look at mediastore.java



It can be seen that there are internal classes such as Audio and Images, which record the names of various fields in the files table. By constructing these parameters, corresponding field values can be inserted and obtained.

MediaStore essentially aliases each field, making it easier to remember and use when coding:

// List some fields: / / picture type MediaStore. The Images. Media. MIME_TYPE / / Audio length MediaStore. Audio. Media. The DURATION / / Video length MediaStore. Video. Media. DURATION // Wait, there are many moreCopy the code

MediaStore and Uri contact



For example, if you want to query an image file in a shared storage space:

Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
Copy the code

MediaStore. Images. Media. EXTERNAL_CONTENT_URI mean to specify the type of the query file is the picture, and construct a Uri object, Uri implements Parcelable, can pass between processes. The receiver (received by another process) matches the Uri, resolves the corresponding field, and performs the specific operation. MediaStore is a class provided by the system to facilitate the operation of shared storage space. If you write your own ContentProvider, you can also define a class similar to MediaStore to mark your own database table fields.

2. Read and write files by Uri

Since you can’t access a file directly through a path, let’s look at how to access a file through a Uri. As mentioned in the previous article, URIs can be obtained through MediaStore or SAF. (Note here that although it is possible to construct urIs directly from the file path, urIs constructed this way do not have access to the file.) Let’s first look at getting urIs through SAF.

Read the file from the Uri

There is now a file named mytest.txt in /sdcard/.

The document reads:

The traditional method of reading mytest.txt directly:

Private void readFile(String filePath) {if (textutils.isEmpty (filePath)) return; try { File file = new File(filePath); FileInputStream fileInputStream = new FileInputStream(file); BufferedInputStream bis = new BufferedInputStream(fileInputStream); byte[] readContent = new byte[1024]; int readLen = 0; while (readLen ! = -1) { readLen = bis.read(readContent, 0, readContent.length); if (readLen > 0) { String content = new String(readContent); Log.d("test", "read content:" + content.substring(0, readLen)); } } fileInputStream.close(); } catch (Exception e) { } }Copy the code

This method is not recommended after the partition storage function is enabled, and permission errors will be reported. TXT files that do not belong to the shared storage space belong to other directories. Therefore, the files cannot be obtained through MediaStore, but can only be obtained through SAF, as follows:

private void startSAF() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); // Specify the file intent.setType("text/plain") for the selected text type; startActivityForResult(intent, 100); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); Uri Uri = data.getData(); if (requestCode == 100) {if (requestCode == 100) {Uri Uri = data.getData(); openUriForRead(uri); }}Copy the code

After taking the Uri, it is used to construct the input stream to read the file.

private void openUriForRead(Uri uri) { if (uri == null) return; InputStream = getContentResolver(). OpenInputStream (uri); byte[] readContent = new byte[1024]; int len = 0; Len = inputStream.read(readContent); if (len ! = -1) { Log.d("test", "read content:" + new String(readContent).substring(0, len)); } } while (len ! = 1); inputStream.close(); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

Final output:

Mytest. TXT belongs to “other directory” file, so it needs to be accessed through SAF. SAF returns Uri, and InputStream is constructed through Uri to read file.

Writes a file from the Uri

Moving on to the writing process, we now need to write to mytest.txt. Again, we need to fetch the Uri through SAF, and construct the output stream once we get the Uri:

private void openUriForWrite(Uri uri) { if (uri == null) { return; } try {// construct OutputStream from uri OutputStream OutputStream = getContentResolver().openoutputstream (uri); String content = "Hello world I'm from SAF\n"; // Write outputStream.write(content.getbytes ()); outputStream.flush(); outputStream.close(); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

Mytest.txt = mytest.txt = mytest.txt = mytest.txt = mytest.txt = mytest.txt

3. Get images and insert albums by Uri

The above lists how to read and write files in other directories by retrieving urIs through SAF. SAF benefits are:

The system provides a file selector. The caller only needs to specify the type of file he/she wants to read and write, such as text type, image type, video type, etc. The selector will filter out the corresponding file for selection. Easy access, simple selection.

Consider another scenario:

If you want to implement the album selector yourself, you need to get the file information in the shared storage space. This is not possible with SAF in this scenario.

Therefore, the key problem is: how to obtain the information of pictures/videos in the shared storage space in batches? The answer is: ContentResolver+ContentProvider+MediaStore(ContentProvider is transparent to the caller). Taking pictures as an example, the insertion and query methods are analyzed. Take a look at the image insertion process:

//fileName is the name of the image to save to the album. Private void insert2Album(InputStream InputStream, String fileName) { if (inputStream == null) return; ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName); If (build.version.sdk_int >= build.version_codes.q) {//RELATIVE_PATH indicates the relative path -------->(1) contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); } else { String dstPath = Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + fileName; / / DATA fields have been abandoned after the Android 10.0 contentValues. Put (MediaStore. Images. ImageColumns. DATA, dstPath); } / / insert photo album -- -- -- -- -- -- -- > (2) the Uri Uri = getContentResolver (), insert (MediaStore. Images. Media. EXTERNAL_CONTENT_URI, contentValues); ------->(3) write2File(uri, inputStream); }Copy the code

Three points are highlighted: (1) before the Android 10.0, MediaStore. Images. ImageColumns. Record the picture absolute path of the DATA field, and the Android 10.0 (including), after the DATA is rejected, Instead use the MediaStore. Images. ImageColumns. RELATIVE_PATH, said relative path. For example, specifying a RELATIVE_PATH to environment. DIRECTORY_PICTURES indicates that subsequent images will be placed in the environment. DIRECTORY_PICTURES directory.

(2) Insert the album by calling the ContentResolver method. MediaStore. Images. Media. EXTERNAL_CONTENT_URI table refers to insert pictures. ContentValues records the field values to be written as a Map. Returns the Uri after insertion.

(3) The above two steps only add a record to the database. The new file pointed to by the record is empty, and the picture needs to be written into the new file. The new file is in the /sdcard/Pictures/ directory, which is not directly accessible through the path, so you need to access it through the Uri returned in step 2.

//inputStream indicates the original file stream. Private void write2File(uri URI, InputStream inputStream) { if (uri == null || inputStream == null) return; OutputStream = getContentResolver().openOutputStream(Uri); byte[] in = new byte[1024]; int len = 0; Len = inputStream.read(in); len = inputStream.read(in); if (len ! = -1) { outputStream.write(in, 0, len); outputStream.flush(); } } while (len ! = 1); inputStream.close(); outputStream.close(); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

Now that you have the Uri associated with the target file, you need the original input file.

Test the above insert method:

private void testInsert() { String picName = "mypic.jpg"; try { File externalFilesDir = getExternalFilesDir(null); File file = new File(externalFilesDir, picName); FileInputStream fis = new FileInputStream(file); insert2Album(fis, picName); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

Where, the original files (pictures) are stored in the private directory of its own external storage -App, as follows:

Note that:

In this example, the original file is stored in the private directory of external storage -app, so the App can use the path to directly read the original file. For other directories, we still need to construct Uri to read, such as through SAF Uri

Get photo

Similarly, to get an image from a system album, you need to access it through a Uri.

private void queryImageFromAlbum() { Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); if (cursor ! = null) {while (cursor.movetonext ()) {// Get a unique id long ID = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); / / structure by id Uri Uri Uri = ContentUris withAppendedId (MediaStore. Images. Media. EXTERNAL_CONTENT_URI, id); // decodeUriForBitmap(uri); } } } private void decodeUriForBitmap(Uri uri) { if (uri == null) return; InputStream = getContentResolver().openInputStream(uri); . / / parse Bitmap Bitmap Bitmap = BitmapFactory decodeStream (inputStream); if (bitmap ! = null) Log.d("test", "bitmap width-width:" + bitmap.getWidth() + "-" + bitmap.getHeight()); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

Similar to the process of inserting an album, you need to take the Uri, construct the input stream, and read the file (image content) from the input stream.

Above, after the analysis of obtaining pictures and inserting albums through Uri, the same process applies to other file types of shared storage space, such as videos, audio files, and downloaded files. Contentresolver.insert (xx)/ contentresolver.query (xx) can also be used in this article.

4. Android 11.0 permission application

Accessing files through URIs seems to have met the requirements of Android 10.0, but there are still some drawbacks:

1. The shared storage space can be accessed only through MediaStore. In the past, the popular access method was directly through the path. For example, the album manager made by myself, first traverse the path of the album to get pictures/videos, and then parse into Bitmap display, now need to get the Uri first, and then parse into Bitmap, somewhat inconvenient. In addition, you may rely on third-party libraries that access files directly through the path, and third-party libraries that have not been updated to fit partitioned storage may not be able to use the corresponding functionality. 2, although SAF can access files in other directories, it needs to jump to a new page to select each time. When it wants to show files in batches, such as the file manager made by itself, it needs to list the directories/files in the current directory, and at this time, it needs to have the permission to traverse /sdcard/ directory. Clearly, the SAF is not up to the job.

Android 11.0 has been optimized to address these issues.

Shared storage space – Media file access changes

Media files can be accessed directly through the path:

private void getImagePath(Context context) { ContentResolver contentResolver = context.getContentResolver(); Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); While (cursor.movetonext ()) {try {// Retrieve path String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); Bitmap bitmap = BitmapFactory.decodeFile(path); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); } break; }}Copy the code

As you can see, access methods that were previously disabled on Android 10.0 are now allowed on Android 11.0, which addresses the first issue above. Note that this method only allows you to read files, but not write files

Google’s official guidance is:

Although it is possible to access media files directly through the path, these operations are ultimately redirected to the MediaStore API, which can cost some performance, and direct access through the path is not necessarily faster than MediaStore API access. In general, it is recommended not to use path access directly unless necessary.

Access all files

If the App has partitioned storage enabled, it will not be able to traverse the /sdcard/ directory when running on an Android 10.0 device. When running on Android 11.0, it can be traversed, requiring the following steps.

1. Declare management rights

Add permission declarations in androidmanifest.xml

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Copy the code

2. Dynamically apply for access permissions for all files

Private void testAllFiles() {// Run device >=Android 11.0 if (build.version.sdk_int >= build.version_codes.r) {// Check whether you already have permission if (! Environment. IsExternalStorageManager ()) {/ / jump a new page to apply for permission to startActivityForResult (new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION), 101); } } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); / / application permissions of results if (requestCode = = 101) {if (Environment) isExternalStorageManager ()) {Toast. MakeText (MainActivity. This, "Request for access to all files successful ", toast.length_short).show(); ShowAllFiles (); }}}Copy the code

You are not prompted to apply for permission in a dialog box, but redirected to a new page, indicating that the permission management is stricter.

3. Traverse directories and read and write files

Once you have the permission, you can perform the corresponding operations.

private void showAllFiles() { File file = Environment.getExternalStorageDirectory(); File[] list = file.listFiles(); for (int i = 0; i < list.length; i++) { String name = list[i].getName(); Log.d("test", "fileName:" + name); }}Copy the code

The file manager effect diagram is similar to the following:

Of course, reading and writing files is not a problem, such as writing files to /sdcard/ :

private void testPublicFile() { File rootFile = Environment.getExternalStorageDirectory(); try { File file = new File(rootFile, "mytest.txt"); FileOutputStream fos = new FileOutputStream(file); String content = "hello world\n"; fos.write(content.getBytes()); fos.flush(); fos.close(); } catch (Exception e) { Log.d("test", e.getLocalizedMessage()); }}Copy the code

ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION The name of this permission looks like it can operate on all files. It breaks the rule of partition storage. It’s not:

Even with this permission, internal storage and external storage -App private directory cannot be accessed

To be clear:

1 and the Environment. IsExternalStorageManager (), the Build. VERSION_CODES. R and needs to be compiled version > = 30 to compile. When using MANAGE_EXTERNAL_STORAGE and targetSdkVersion>=30, the App is banned from Google Play until 2021 at the earliest. Therefore, if you have applied for MANAGE_EXTERNAL_STORAGE permission before this time, it is best not to upgrade targetSdkVersion beyond 30.

5, Android 10/11 storage adaptation suggestions

Ok, by analyzing Android 10/11 storage adaptation, we have learned how different systems need to adapt, so we need a unified adaptation solution.

Adapter core

Partition storage is the core. Files generated by App itself should be stored in their own directories:

/ sdcard/Android/data/packagename/and/data/data/packagename /

This App can apply for these two directories without applying for access permission, and other apps cannot access the directories of this App.

Adapting shared Storage

Files in the shared storage space need to be accessed through Uri input and output streams. There are two ways to obtain Uri: MediaStore and SAF.

Adapt to other directories

On Android 11, you need to apply for access to all files.

The specific practices

The first step

Add the following fields to androidmanifest.xml:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Copy the code

Add the following fields under the label:

android:requestLegacyExternalStorage="true"
Copy the code

The second step

If you need to access the shared storage space, check whether the running device version is Android6.0 or higher. If yes, apply for the WRITE_EXTERNAL_STORAGE permission. Once you have the permission, access the files in the shared storage space using the Uri. If you need to access other directories, access them through SAF

The third step

If you want to do file manager, virus scan manager and other functions. Check whether the running device version is greater than or equal to Android 6.0. If yes, apply for common storage rights first. If you are running Android 10.0, you can access files under /sdcard/ directly (because partition storage is disabled). If the device version is Android 11.0, apply for the MANAGE_EXTERNAL_STORAGE permission.

That’s all about Android storage permission adaptation.

This article is based on Android 10.0 11.0. Android 10.0 real machine, Android 11.0 simulator test code

If you like, please like, pay attention to your encouragement is my motivation to move forward

Continue to update, with me step by step system, in-depth study of Android