Android10 pit-filling adaptation guide, including actual experience code, never copy translated documents
1.Region.Op相关异常:java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed
The exception caused by calling canvas.clipPath(path, Region.Op.XXX ); when targetSdkVersion >= Build.VERSION_CODES.P , the reference source code is as follows:
@Deprecated
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
checkValidClipOp(op);
return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
}
private static void checkValidClipOp(@NonNull Region.Op op) {
if (sCompatiblityVersion >= Build.VERSION_CODES.P
&& op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
throw new IllegalArgumentException(
"Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
}
}
We can see that when the target version starts from Android P, Canvas.clipPath( @NonNull Path path, @NonNull Region.Op op); has been deprecated and is an abandoned API that contains exception risks. Only Region.Op.INTERSECT and Region .Op.DIFFERENCE is compatible, and almost all blogging solutions are as simple and crude as this:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
canvas.clipPath(path);
} else {
canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等
}
But what if we must need some advanced logical operation effects? For example, to simulate the page-turning reading effect of a novel, the solution is as follows, use Path.op instead, calculate the Path first, and then give canvas.clipPath:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
Path mPathXOR = new Path();
mPathXOR.moveTo(0,0);
mPathXOR.lineTo(getWidth(),0);
mPathXOR.lineTo(getWidth(),getHeight());
mPathXOR.lineTo(0,getHeight());
mPathXOR.close();
//以上根据实际的Canvas或View的大小,画出相同大小的Path即可
mPathXOR.op(mPath0, Path.Op.XOR);
canvas.clipPath(mPathXOR);
}else {
canvas.clipPath(mPath0, Region.Op.XOR);
}
2. Clear text HTTP restrictions
When targetSdkVersion >= Build.VERSION_CODES.P , HTTP requests are restricted by default, and relevant logs appear:
java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy
The first solution: Add the following node code to Application in AndroidManifest.xml
<application android:usesCleartextTraffic="true">
The second solution: Create a new xml directory in the res directory. Skip the existing one. Create a new xml file network_security_config.xml in the xml directory, and then add the following node code to Application in AndroidManifest.xml.
android:networkSecurityConfig="@xml/network_config"
The name is random and the content is as follows:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
3. Media resource reading and writing in Android Q
1. Scan system albums, videos, etc. The picture and video selectors are provided through ContentResolver. The main code is as follows:
private static final String[] IMAGE_PROJECTION = {
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media._ID,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
Cursor imageCursor = mContext.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[4] + " DESC");
String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
int id = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));
String folderPath = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[3]));
String folderName = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[4]));
//Android Q 公有目录只能通过Content Uri + id的方式访问,以前的File路径全部无效,如果是Video,记得换成MediaStore.Videos
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
path = MediaStore.Images.Media
.EXTERNAL_CONTENT_URI
.buildUpon()
.appendPath(String.valueOf(id)).build().toString();
}
2. Determine whether the public directory file exists. Since Android Q, the public directory File API has become invalid. You cannot directly determine whether the public directory file exists through new File(path).exists();. The correct method is as follows:
public static boolean isAndroidQFileExists(Context context, String path){
AssetFileDescriptor afd = null;
ContentResolver cr = context.getContentResolver();
try {
Uri uri = Uri.parse(path);
afd = cr.openAssetFileDescriptor(uri, "r");
if (afd == null) {
return false;
} else {
close(afd);
}
} catch (FileNotFoundException e) {
return false;
}finally {
close(afd);
}
return true;
}
3. Copy or download the file to the public directory. The same applies to saving Bitmap. For example, Download, MIME_TYPE type can refer to the corresponding file type. Here we only explain the APK. Copy from the private directory to the public directory demo as follows (the same applies to remote downloading, Just get the OutputStream, or you can download it to a private directory and copy it to the public directory):
public static void copyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + saveDirName.replaceAll("/","") + "/");
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
Uri insertUri = resolver.insert(external, values);
if(insertUri == null) {
return;
}
String mFilePath = insertUri.toString();
InputStream is = null;
OutputStream os = null;
try {
os = resolver.openOutputStream(insertUri);
if(os == null){
return;
}
int read;
File sourceFile = new File(sourcePath);
if (sourceFile.exists()) { // 文件存在时
is = new FileInputStream(sourceFile); // 读入原文件
byte[] buffer = new byte[1444];
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
close(is,os);
}
}
4. Save pictures related
/**
* 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广播告诉系统插入相册
*
* @param context context
* @param sourceFile 源文件
* @param saveFileName 保存的文件名
* @param saveDirName picture子目录
* @return 成功或者失败
*/
public static boolean saveImageWithAndroidQ(Context context,
File sourceFile,
String saveFileName,
String saveDirName) {
String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath());
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
values.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.TITLE, "Image.png");
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + saveDirName);
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
Uri insertUri = resolver.insert(external, values);
BufferedInputStream inputStream = null;
OutputStream os = null;
boolean result = false;
try {
inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
if (insertUri != null) {
os = resolver.openOutputStream(insertUri);
}
if (os != null) {
byte[] buffer = new byte[1024 * 4];
int len;
while ((len = inputStream.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
}
result = true;
} catch (IOException e) {
result = false;
} finally {
close(os, inputStream);
}
return result;
}
4.EditText does not gain focus by default and does not automatically pop up the keyboard.
This problem occurs when targetSdkVersion >= Build.VERSION_CODES.P , and the device version is Android P or above. The solution is to add the following code to onCreate to gain focus. If you need to pop up the keyboard, you can delay it:
mEditText.post(() -> {
mEditText.requestFocus();
mEditText.setFocusable(true);
mEditText.setFocusableInTouchMode(true);
});
5. Install APK Intent and other shared file-related Intents
/*
* 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行了限制,只能通过Uri来操作,
* 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION
*/
private void installApk() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
//适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(mFilePath) ,"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
return ;
}
File file = new File(saveFileName + "demo.apk");
if (!file.exists())
return;
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "net.oschina.app.provider", file);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
6.Activity transparency related, windowIsTranslucent attribute
Android Q is another sinkhole. If you want to display a translucent Activity, before Android 10, the normal style Activity only needs to set windowIsTranslucent=true, but in Android Q, it has no effect, and if you dynamically set View.setVisibility( ), there will still be residual images on the interface...
Solution: Use Dialog style Activity and set windowIsFloating=true. The problem arises again. If the Activity root layout does not set fitsSystemWindow=true, the status bar will not be invaded by default, making the interface look normal.
7. Clipboard compatible
In Android Q, only when the application is in an interactive state (the default input method itself is interactive) can the clipboard be accessed and the clipboard changes monitored. The clipboard cannot be directly accessed during the onResume callback. The advantage of this is that it avoids some The background of the application frantically monitors and responds to the contents of the clipboard and pops up windows crazily.
Therefore, if you still need to monitor the clipboard, you can use the application life cycle callback, monitor the APP background return, delay access to the clipboard for a few milliseconds, then save the clipboard content obtained from the last access, and compare whether there are changes each time. Then proceed to the next step.
8. When third-party operations such as sharing images directly use file paths, such as QQ image sharing, please note that this is not feasible and can only be operated by obtaining the Uri through APIs such as MediaStore.
This article is reproduced from: https://blog.csdn.net/huanghaibin_dev/article/details/103237821