This article is published simultaneously on my wechat public account. You can follow it by searching “Guo Lin” on wechat. The article is updated every weekday.
It’s been more than six months since Android 10 was released. Have your apps adapted to it?
Among Android 10’s many behavioral changes, one that deserves our attention is scoped storage. This new feature directly overturns the way we have used external storage space for a long time, so many apps will face the upgrade of many code modules.
However, there is not much official information about the new feature of scoped storage, and many people do not understand its use. In addition, it does not belong to the existing knowledge architecture system of “Line 1 Code”. Although I have thought about adding this part to the third edition, I have decided to explain this part in a separate article after a few thoughts, which can also be regarded as an extension of “Line 1 Code Third Edition”.
This article has taken a thorough look at scoped storage, and you should be able to easily adapt to Android 10 scoped storage.
Understand scoped storage
Android has long supported external storage, also known as SD card storage. This function is so widely used that almost all apps like to create their own directory under the root directory of the SD card for storing all kinds of files and data.
So what’s the benefit of that? I thought about it. Two things. First, files stored on an SD card don’t count toward your app’s footprint, which means that even if you have 1 GIGAByte of files stored on an SD card, your app’s footprint may only show up in tens of kilobytes of space. Second, files stored on the SD card will still be retained even if the application is uninstalled, which helps to implement some functions that require data to be retained permanently.
But are these “benefits” really benefits? Maybe it’s good for developers, but for users, it’s nothing short of rogue behavior. It messes up the user’s SD card space, and even if I uninstall a program that I no longer use at all, the resulting junk files may remain on my phone forever.
In addition, the files stored on the SD card are public files, and all applications have the right to access them at will, which also poses great challenges to data security.
To address these issues, Google has added scoped storage to Android 10.
So what exactly is scoped storage? To put it simply, the Android system has a very limited use of SD cards. Since Android 10, each application can only be entitled to in their own external storage associated directory read and create a file, to get the associated directory of the code is: context. GetExternalFilesDir (). The path of the associated directory is as follows:
/ storage/emulated / 0 / Android/data / < package name > / filesCopy the code
By storing data in this directory, you can read and write files exactly as before, with no changes or adaptations required. But at the same time, those two “benefits” are gone. Files in this directory are counted toward the application’s footprint and are deleted when the application is uninstalled.
So some friends may ask, I just need to access other directories what should I do? Such as reading a picture in the phone album, or adding a picture to the phone album. For this purpose, the Android system classifies file types. Images, audio, and video files can be accessed through the MediaStore API, while other types of files need to be accessed using the system’s file picker.
In addition, any image, audio, or video that our application contributes to the media library will automatically have read and write permissions. We do not need to apply for READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions. If you want to read images, audio, or video that other applications contribute to the media library, you must apply for the READ_EXTERNAL_STORAGE permission. The WRITE_EXTERNAL_STORAGE permission will be deprecated in future Android versions.
Ok, so much for the theory of scoped storage, I believe you have a basic understanding of it, so let’s get started.
Do I have to upgrade?
There must be a lot of friends concerned about this problem, because whenever the adaptation upgrade is faced with the need to change a lot of code, most people’s first thought is to not upgrade at all possible, or late upgrade at all possible. In the case of scoped storage, congratulations, there is no need to upgrade for the time being.
Currently, Android 10 is not so strict about scoped storage adaptation, because the traditional external storage space is so widely used. If your project specifies targetSdkVersion less than 29, your project will run successfully on Android 10 phones without any scoped storage adaptation.
If your targetSdkVersion is already set to 29, it doesn’t matter. If you don’t want to implement scoped storage adaptation, just add the following configuration to androidmanifest.xml:
<manifest ... >
<application android:requestLegacyExternalStorage="true" ...>
...
</application>
</manifest>
Copy the code
This configuration indicates that, even on Android 10, it is still allowed to use the legacy use of external storage to run applications without making any changes to the code. Of course, this is just a stopgap, and it could be disabled at any time in a future Version of Android. (The scoped storage was mandatory in Android 11, and this configuration is no longer valid in Android 11.) Therefore, it is important that we learn how to adapt scoped storage now.
In addition, the source code for all the examples demonstrated in this article can be found in ScopedStorageDemo, an open source library.
Get the picture in the album
First, let’s learn how to get photos from your phone’s album in scope storage. Note that although I used images as an example in this article, the use of audio and video is basically the same.
In scoped storage, we can only use the MediaStore API to get the Uri of the image, as shown in the following code:
val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc") if (cursor ! = null) { while (cursor.moveToNext()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) println("image uri is $uri") } cursor.close() }Copy the code
In the above code, we get the ids of all the images in the album using ContentResolver, and then use ContentUris to assemble the ids into a complete Uri object. The Uri format for an image looks something like this:
content://media/external/images/media/321
Copy the code
So some of you might be asking, how do I display this image once I get the Uri? This can be done in a number of ways, such as using Glide to load images, which itself supports passing in Uri objects as image paths:
Glide.with(context).load(uri).into(imageView)
Copy the code
If you don’t use Glide or another image-loading framework and want to parse a Uri object directly into an image without using a third-party library, you can use the following code:
val fd = contentResolver.openFileDescriptor(uri, "r") if (fd ! = null) { val bitmap = BitmapFactory.decodeFileDescriptor(fd.fileDescriptor) fd.close() imageView.setImageBitmap(bitmap) }Copy the code
In the code above, we call the openFileDescriptor() method of the ContentResolver and pass in the Uri object to open the file handle. Then call the decodeFileDescriptor() method of BitmapFactory to parse the file handles into Bitmap objects.
The Demo:
This way we’ve mastered how to retrieve photos from albums, and it works on all Android versions.
So next, let’s learn how to add an image to an album.
Add a picture to an album
Adding an image to a mobile album is a bit more complicated, because it’s handled differently from one version to the next.
To get a sense of this, let’s use a code example like this:
fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) { val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) } else { values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName") } val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) if (uri ! = null) { val outputStream = contentResolver.openOutputStream(uri) if (outputStream ! = null) { bitmap.compress(compressFormat, 100, outputStream) outputStream.close() } } }Copy the code
This code demonstrates how to add a Bitmap object to your phone’s photo album. Let me explain briefly.
To add an image to a mobile photo album, we need to build a ContentValues object and add three important pieces of data to this object. One is DISPLAY_NAME, which is the name of the image to display, and the other is MIME_TYPE, which is the MIME type of the image. There is also the path to the image, but this value is handled differently in Android 10 than in previous versions of the system. Android 10 added a RELATIVE_PATH constant that represents the relative path of the file store, Possible values are DIRECTORY_DCIM, DIRECTORY_PICTURES, DIRECTORY_MOVIES, and DIRECTORY_MUSIC, which indicate albums, pictures, movies, and music directories respectively. In previous versions we didn’t have a RELATIVE_PATH, so we used the DATA constant (deprecated in Android 10) and pieced together an absolute path to the file store.
Once you have the ContentValues object, you can then call the Insert () method of the ContentResolver to get the Uri to insert the image. But it’s not enough just to get the Uri, we also need to write data to the image that the Uri corresponds to. Call the openOutputStream() method of the ContentResolver to get the output stream of the file, and then write the Bitmap into that output stream.
If the image I want to store is not a Bitmap, but an image from the network, or an image from the current app associated directory, what should I do?
In fact, the method is similar, because no matter the picture on the network or the picture under the associated directory, we can get its input stream, as long as we keep reading the data in the input stream, and then write it into the output stream corresponding to the photo album, the example code is as follows:
fun writeInputStreamToAlbum(inputStream: InputStream, displayName: String, mimeType: String) { val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) } else { values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName") } val bis = BufferedInputStream(inputStream) val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) if (uri ! = null) { val outputStream = contentResolver.openOutputStream(uri) if (outputStream ! = null) { val bos = BufferedOutputStream(outputStream) val buffer = ByteArray(1024) var bytes = bis.read(buffer) while (bytes >= 0) { bos.write(buffer, 0 , bytes) bos.flush() bytes = bis.read(buffer) } bos.close() } } bis.close() }Copy the code
In this code, only the input stream and output stream parts are rewritten. The rest of the code is exactly the same as the previous Bitmap storage code. I believe it is easy to understand.
The Demo:
Now that we’ve solved the problem of loading and storing album images, let’s look at another common requirement, how to Download files to the Download directory.
Download the file to the Download directory
Performing file download operations is a common scenario, such as downloading PDF or doc files, or downloading APK installation packages, etc. In the past, we used to Download these files to the Download directory, which is a dedicated directory for downloading files. Starting with Android 10, we can no longer access external storage in an absolute path, so file downloads will also suffer.
So how to solve it? There are two main ways.
The first and easiest way is to change the download directory of the file. Download the files to the associated directory of the application, so that the application can work properly on Android 10 without changing any code. But this way, you need to know that the downloaded file will be counted toward the application’s footprint and deleted if the application is uninstalled. In addition, files stored in the associated directory can only be accessed by the current application, other programs are not read permission.
If the above limitations do not satisfy your needs, then you will have to use the second method, which is to adapt the Android 10 code and still Download the files to the Download directory.
Downloading a file to a Download directory is similar to adding an image to an album, but Android 10 has added a Downloads collection in MediaStore for file Downloads. However, since the implementation of the download functionality is different from project to project, and some projects are quite complex, it is up to you to figure out how to incorporate the following sample code into your project.
fun downloadFile(fileUrl: String, fileName: String) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Toast.makeText(this, "You must use device running Android 10 or higher", Toast.LENGTH_SHORT).show() return } thread { try { val url = URL(fileUrl) val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connectTimeout = 8000 connection.readTimeout = 8000 val inputStream = connection.inputStream val bis = BufferedInputStream(inputStream) val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) if (uri ! = null) { val outputStream = contentResolver.openOutputStream(uri) if (outputStream ! = null) { val bos = BufferedOutputStream(outputStream) val buffer = ByteArray(1024) var bytes = bis.read(buffer) while (bytes >= 0) { bos.write(buffer, 0 , bytes) bos.flush() bytes = bis.read(buffer) } bos.close() } } bis.close() } catch(e: Exception) { e.printStackTrace() } } }Copy the code
The code as a whole is fairly straightforward, adding some Http request code and changing mediastore.images.media to Mediastore.downloads. The rest is pretty much the same, and I won’t explain it too much.
Note that the code above will only run on Android 10 or higher, as Mediastore.Downloads is a new API added to Android 10. For Android 9 and below, please use the same code to download the file.
The Demo:
Use the file selector
If we want to read non-image, audio, or video files on the SD card, such as a PDF file, we can no longer use the MediaStore API, but use the file selector.
But instead of writing a file browser and picking files from it, you have to use the built-in file picker on your phone. Example code is as follows:
const val PICK_FILE = 1 private fun pickFile() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "*/*" startActivityForResult(intent, PICK_FILE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { PICK_FILE -> { if (resultCode == Activity.RESULT_OK && data ! = null) { val uri = data.data if (uri ! = null) { val inputStream = contentResolver.openInputStream(uri) } } } } }Copy the code
The pickFile() method launches the system’s file selector with an Intent. Note that the action and category of the Intent are fixed. The type attribute can be used to filter file types. For example, if you specify image/, only image files can be displayed. In this case, it is written as /* to show all file types. Note that the type attribute must be specified, otherwise it will crash.
Then, in the onActivityResult() method, we get the Uri of the file selected by the user, which is then read by opening the file input stream via the ContentResolver.
The Demo:
By the end of this article, you’ll have a pretty good idea of how to use and adapt the Android 10 scoped store. However, we may also face a headache in the actual development work, that is, my own code can adapt, but the third party SDK used in the project does not support scoped storage. What should I do?
This situation does exist, for example, I used the qiniuyun SDK before, its file upload function requires that you pass in an absolute path of a file, but does not support passing in Uri objects, you should also encounter similar problems.
Since we do not have the permission to modify the third-party SDK, the simplest and direct way is to wait for the third-party SDK provider to update this part of the function. Before that, we do not specify targetSdkVersion to 29. Or in the AndroidManifest file configuration first requestLegacyExternalStorage properties.
If you don’t want to use the expedient measure, however, there are a very good way to solve this problem, is ourselves. Write a file copy function, Uri object file is copied to the application of the associated directory, then the association the absolute path to the directory of the file transfer to a third party SDK, And then it fits perfectly. Example code for this feature is as follows:
fun copyUriToExternalFilesDir(uri: Uri, fileName: String) { val inputStream = contentResolver.openInputStream(uri) val tempDir = getExternalFilesDir("temp") if (inputStream ! = null && tempDir ! = null) { val file = File("$tempDir/$fileName") val fos = FileOutputStream(file) val bis = BufferedInputStream(inputStream) val bos = BufferedOutputStream(fos) val byteArray = ByteArray(1024) var bytes = bis.read(byteArray) while (bytes > 0) { bos.write(byteArray, 0, bytes) bos.flush() bytes = bis.read(byteArray) } bos.close() fos.close() } }Copy the code
Well, that’s all you need to know about Android 10 scoped storage. In the next article, we will continue to learn about Android 10 adaptation, talk about the function of dark theme, see the link: Android 10 adaptation key points, dark theme.
Note: The source code for all the examples demonstrated in this article can be found in ScopedStorageDemo, an open source library.
The open source library address is github.com/guolindev/S…
This article is an extension of Line 1, Version 3, which is out now. Kotlin, Jetpack, and MVVM are all available here.
Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles pushed every day.