preface
Our company’s APP is always shown not compatible with EMUI10.0.0 system in Huawei application market. Consult the customer service staff and send us the official Google adaptation document.
After applying 10 to the market, contact huawei market customer service. As expected, the prompt is not compatible.
When it comes to the 10 system adaptation, of course, it’s the storage! Although Google wechat public account pushed the following article:
Being a serious coder, I also decided to have some fun with the MediaStore API (in fact, it was only because I had already adapted it that I was sent this compatibility solution… 😂)
The 10 system has been out for a long time, and its storage has been explained in detail by many big guys’ blogs. Today, I won’t say much about the concept. Next, I will share the adaptation experience combined with the functions of the APP.
Because our company’s APP iteration has been 3 years, the old code is Java, the new code is Kotlin, the following code will appear Java (old) and Kotlin (new) interlusion, I am too lazy to unify, hope you can understand.
If there is a place where the adaptation is not in place, I hope you will point out
Photo album features
The first step is to grab the pictures and videos in the phone and go directly to the code:
ContentResolver contentResolver = context.getContentResolver();
// Edit in descending order, i.e. the latest shot is in front
String sort = MediaStore.Images.Media.DATE_MODIFIED + " desc ";
String selection = MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?";
String[] selectionArgs = new String[]{"image/jpeg"."image/png"};// Find only JPG and PNG images
String[] projection = {MediaStore.Images.Media._ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,// The name of the folder? I use it to sort by folder anyway
MediaStore.Images.ImageColumns.DATE_MODIFIED};
Cursor cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sort);
if(cursor ! =null) {
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
String bucketName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME));
if (TextUtils.isEmpty(bucketName)) continue;// Make a filter
/ / the key to the android. Content. Turn android.net.Uri ContentUris provide API
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
// Omit the business logic
cursor.close();
}
Copy the code
The Video retrievals are similar to the images. Here I give you the query logic. Note that the Video uses mediastore.video.media:
String[] project = new String[]{MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DURATION,// The length of the video
MediaStore.Video.Media.DATE_MODIFIED};
Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
project,
MediaStore.Video.Media.MIME_TYPE + "=?".new String[]{"video/mp4"},// Here only look up the MP4 format
MediaStore.Video.Media.DATE_MODIFIED + " desc");
Copy the code
The above code captures the key android.net.Uri, so how do you display pictures and videos? At this point I’m going to blow up Glide (which, by the way, can load the Uri of the video)
Glide version 4.9.0 supports Uri passing parameters, and the latest version 4.11.0 supports uri. toString() (the latest version is strongly recommended). Glide internally identifies your String arguments as “network address”, “path to File”, “String of Uri”, etc. So our business layer doesn’t really need to care about these String distinctions.
This time, change uri.toString () to so easy. And this set of display logic written down, is basically compatible with the low version! Why do you say that? Because:
I have tested the Night God simulator (5.1 system), Samtisan and OPPO and Huawei (7.1 system), Xiaomi (9 system), Huawei (10 system), and Glide uri.toString () loads without any problems! So Glide’s internal loading mechanism is worth a look! Of course, that’s not today’s topic (mainly because I can’t figure it out myself).
Image cropping
The image cropping library used by our APP does not support Uri parameter passing, so I chose to copy the image to the private directory of the APP first, and then pass the parameter of File. To copy the image, go straight to the code (we also did the image compression step) :
InputStream originInputStream = null;
InputStream composeInputStream = null;
ByteArrayOutputStream outputStream = null;
FileOutputStream fileOutputStream = null;
try {
// Start the Uri operation to File
ContentResolver contentResolver = context.getContentResolver();
if(contentResolver ! =null) {
/ / contentResolver openInputStream concrete implementation can be a little bit see out there are several kinds of the format of the Uri
originInputStream = contentResolver.openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 1;
BitmapFactory.decodeStream(originInputStream, null, options);
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
//computeSize is our compression logic and can be ignored
options.inSampleSize = BitmapUtil.computeSize(srcWidth, srcHeight);
// Get the real picture
options.inJustDecodeBounds = false;
composeInputStream = contentResolver.openInputStream(uri);
Bitmap tagBitmap = BitmapFactory.decodeStream(composeInputStream, null, options);
outputStream = new ByteArrayOutputStream();
tagBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
// Some business logic requires size
int[] size = new int[]{tagBitmap.getWidth(), tagBitmap.getHeight()};
tagBitmap.recycle();
// Write the File to the private directory File
fileOutputStream = new FileOutputStream(destFile);
fileOutputStream.write(outputStream.toByteArray());
fileOutputStream.flush();
fileOutputStream.close();
returnsize; }}catch (IOException e) {
e.printStackTrace();
} finally {
// Close the I/O stream here
}
Copy the code
Video editing
For our videos, we used a penguin video processing SDK (for a fee), which is guaranteed to be compatible with Android 10, so the video part didn’t fit well. So I suggest you find a real one.
The only downside is that the SDK-edited video output File seems to only support the File Api, so on Android 10 I just exported it to a private directory and copied it to the album directory. Because the video we edited can be found by other apps.
As for how to copy a video from a private directory to a public directory, this will be covered in the following section on downloading.
File download
At first I naively assumed that file downloads could be directly compatible with Android 10 and below, just write a set of code. But reality hit me hard! So I used version judgment.
10 System or above
Let’s talk about Android 10 and above, let’s first judge the conditions:
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) / / is greater than 9.0
Copy the code
Retrofit2+RxJava2+OkHttp4 is used in the project, and by the way OK4 can be simply understood as the Kotlin version of OK3. Abstract class ResponseBody: Closeable (kotlin syntax)
It doesn’t matter what the ResponseBody is (actually because I can’t…) What matters is how to operate it:
val inputSystem = responseBody.byteStream()/ / into Java. IO. InputStream
val contentLength = responseBody.contentLength()// Take a length to calculate the progress
// Start!
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
BufferedInputStream will be internally close()
UriOperator.write(BufferedInputStream(inputSystem), saveFileName, saveType, object: WriteListener < Uri > {private var localLength: Long = 0L// Keep a record of writing progress
// Calculate the progress
override fun onWriting(length: Long) {
localLength += length
RxSchedulers.scheduleMainThread {// Self-encapsulates switching to the main thread to notify the caller
this@AsyncDownloadCallback.downloadProgress(localLength, contentLength)
}
}
// Download failed
override fun onFailed(code: Int, msg: String) {
//closeQuietly() from package okhttp3.internal util. kt
inputSystem.closeQuietly()
RxSchedulers.scheduleMainThread {
this@AsyncDownloadCallback.onFail(null, DownloadException(code, msg))
}
}
override fun onSuccess(data: Uri) {
inputSystem.closeQuietly()
RxSchedulers.scheduleMainThread {
this@AsyncDownloadCallback.onSuccess(data)}}})}else {
//10
}
Copy the code
Let’s look at the urioperator. write method:
// The outer layer of this method is called asynchronously
fun write(bufferedInputStream: BufferedInputStream, saveFileName: String.@FileSaveType fileType: String, listener: WriteListener<Uri>? {
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {// Check whether the memory card is availablegetInsertUri(saveFileName, fileType)? .let { uri ->// Find the Uri based on fileTypeFileSystem.context.contentResolver.openOutputStream(uri)? .let {if (writeIO(bufferedInputStream, it, object : IOListener {
override fun operator(length: Long){ listener? .onWriting(length) } })) { listener? .onSuccess(uri) }else{ listener? .onFailed(ERROR_IO_EXCEPTION,"Write exception!")}}? : listener? .onFailed(ERROR_OUTPUT_NULL,"Unable to open output stream!")
} ?: listener?.onFailed(ERROR_UNKNOW_TYPE, "Unknown file type!")}else {
// If necessary, this can be transferred to a private directorylistener? .onFailed(ERROR_NO_SDCARD,"No external storage!")}}Copy the code
GetInsertUri (saveFileName, fileType); @filesaveType is my own type annotation:
fun getInsertUri(saveFileName: String.@FileSaveType fileType: String) = when (fileType) {
SAVE_JPG, SAVE_PNG -> {// Image format
FileSystem.context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues().apply {
this.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName.plus(".").plus(if (fileType == SAVE_JPG) "jpg" else "png"))
this.put(MediaStore.Images.Media.MIME_TYPE, if (fileType == SAVE_JPG) "image/jpg" else "image/png")
// You can specify a subfolder under images
//this.put(MediaStore.Images.Media.RELATIVE_PATH, "MyPic")
// On devices running Android 10 (API level 29) and higher, your application can gain exclusive access to media files when they are written to disk by using the IS_PENDING flag.
//this.put(MediaStore.Images.Media.IS_PENDING, 1)
}}}}}}}}}}}}}}}}}}}} So I'm useless here, I don't have the need yet
})
}
SAVE_MP4 -> {
FileSystem.context.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, ContentValues().apply {
this.put(MediaStore.Video.Media.DISPLAY_NAME, saveFileName.plus(".mp4"))
this.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
})
}
SAVE_APK -> {
FileSystem.context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, ContentValues().apply {
this.put(MediaStore.Downloads.DISPLAY_NAME, saveFileName.plus(".apk"))
this.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive")})}else -> null
}
Copy the code
Finally, look at the writeIO method:
fun writeIO(bufferedInputStream: BufferedInputStream, outputStream: OutputStream, listener: IOListener? = null): Boolean {
try {
var len: Int
bufferedInputStream.use { input ->
outputStream.use { output ->
val buffer = ByteArray(1024)
while(input.read(buffer).also { len = it } ! =- 1) {
output.write(buffer, 0, len) output.flush() listener? .operator(len.toLong())// Return the write progress}}}}catch (t: Throwable) {
return false
}
return true
}
Copy the code
The above process runs down, we can download the file! Unfortunately, the compatibility of the earlier version is not very good.
On the 7.0 system I tested, when I opened the videos I downloaded through the system album, I found that attributes like duration and resolution were missing. After all, I didn’t know these properties when I downloaded them.
Of course, once the file is downloaded you can read the properties and write them in using the ContentResolver API.
Read the video attribute, I recommend using android. Media. MediaMetadataRetriever said there is not much.
Here’s how to copy files from a private directory to a common directory:
val stream = FileInputStream(file)
// The above write method
write(BufferedInputStream(stream) ...)
Copy the code
10 Under the System
Because there was a code for File download in the project before, I simply lazy 😜 directly, or go the previous way under 10.
Here’s how to get the File directory:
Screenshots here is to want to give you look at the Environment when targetSdkVersion = 29. GetExternalStoragePublicDirectory this has already been abandoned. I don’t know if it will be deleted… 😓
Then the data will be downloaded to scan, this album APP to see pictures, such as used here is android. Media. MediaScannerConnection, accept the File transfer and we have to do is:
public static void mediaScan(Context context, File file, String mediaType) {
MediaScannerConnectionClientImpl impl = new MediaScannerConnectionClientImpl(file, mediaType);
MediaScannerConnection mediaScannerConnection = new MediaScannerConnection(context, impl);
impl.setMediaScannerConnection(mediaScannerConnection);
mediaScannerConnection.connect();
}
public static class MediaScannerConnectionClientImpl implements MediaScannerConnection.MediaScannerConnectionClient {
private MediaScannerConnection mediaScannerConnection;
private File file;
private String mediaType;
MediaScannerConnectionClientImpl(File file, String mediaType) {
this.file = file;
this.mediaType = mediaType;
}
void setMediaScannerConnection(MediaScannerConnection mediaScannerConnection) {
this.mediaScannerConnection = mediaScannerConnection;
}
@Override
public void onMediaScannerConnected(a) {
if(mediaScannerConnection ! =null)
mediaScannerConnection.scanFile(file.getAbsolutePath(), mediaType);
}
@Override
public void onScanCompleted(String path, Uri uri) { mediaScannerConnection.disconnect(); }}Copy the code
As for whether MediaScanner can swipe URIs in the 10 system, you can try it yourself… 😂
Since MediaScanner is actually bound to a service, I recommend starting it with an Application, which can leak the Activity (which I’ve already encountered).
Some of the other
File upload
Our APP is uploaded in the private directory, that is to say, the File API can continue to use, hey! My idea is to convert URIs into IO streams, in a word, the actual coding is a little bit troublesome, this part has not been practiced.
share
Sharing we use the SHARING SDK, because our business is simple, the current picture sharing is based on network pictures. Amu should be downloading the images to a private directory (test conclusion), so far no problem.
In-app updates
APK is downloaded to a private directory, this is the old way, the test is no problem.
Photo was taken
At present, it is the old way to take the private directory, and the test is no problem.
reference
Google Developers: An update to the storage mechanism in Android 11
Official: Handles media files in external storage
Android 10 Adaptation Guide