Android版本适配需要注意的坑

一、Android 8.0 适配 -- targetSdkVersion 升级成26 需要注意的一些坑

1. MODE_WORLD_READABLE 模式(表示当前文件可以被其他应用读取)  废弃 (ContentProvider、BroadcastReceiver、Service、SharedPreferences)

Caused by: java.lang.SecurityException: MODE_WORLD_READABLE no longer supported


Android api 24后,对于文件权限进行了限制。 类似苹果的沙盒模式,应用创建的文件夹,其他应用无权限访问

所以如果原代码中有使用MODE_WORLD_READABLE 模式的要替换成 MODE_PRIVATE 模式

2. 获取通话记录

Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.CallLogProvider from ProcessRecord{8c75d80 31624:com.ct.client/u0a122} (pid=31624, uid=10122) requires android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG

针对android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG做权限适配

3、图片选择和裁剪(通过FileProvider实现应用间共享文件)

Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.ct.client/files/com.ct.client/camere/1547090088847.jpg exposed beyond app through ClipData.Item.getUri()  

在AndroidManifest.xml清单文件中注册 provider

<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.xx.xxx.fileProvider" android:grantUriPermissions="true" android:exported="false">

<!--元数据-->

<meta-data android:name="android.support.FILE_PROVIDER_PATH" android:resource="@xml/file_paths" />

</provider>

需要注意一下几点:

exported:必须为false

grantUriPermissions:true,表示授予 URI 临时访问权限。

authorities 组件标识,都以包名开头,避免和其它应用发生冲突。

第二步:

指定共享文件的目录,需要在res文件夹中新建xml目录,并且创建file_paths

<resources xmlns:android="http://schemas.android.com/apk/res/android">

<paths>

<external-path path="" name="download"/>

</paths>

</resources>

path=”“,是有特殊意义的,它代表根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。

name="", 指对应目录下的对应的文件

第三步:

使用FileProvider

根据版本号把Uri改成使用FiliProvider创建的Uri,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

cameraFileUri = FileProvider.getUriForFile(mContext, "com.xx.xxx.fileProvider[authorities 对应的值]", new File(saveCamerePath, saveCameraFileName));

} else {

cameraFileUri = Uri.fromFile(new File(saveCamerePath, saveCameraFileName));

}

添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)来对目标应用临时授权该Uri所代表的文件

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //添加这一句表示对目标应用临时授权该Uri所代表的文件 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); }

在设置裁剪要保存的 intent.putExtra(MediaStore.EXTRA_OUTPUT, outUri);的时候,这个outUri是要使用Uri.fromFile(file)生成的,而不是使用FileProvider.getUriForFile。

FileProvider所支持的几种path类型

从Android官方文档上可以看出FileProvider提供以下几种path类型:

<files-path path="" name="camera_photos" />

该方式提供在应用的内部存储区的文件/子目录的文件。它对应Context.getFilesDir返回的路径:eg:"/data/data/com.xx.xxx/files"。

<cache-path name="name" path="path" />

该方式提供在应用的内部存储区的缓存子目录的文件。它对应getCacheDir返回的路径:eg:“/data/data/com.xx.xxx/cache”;

<external-path name="name" path="path" />

该方式提供在外部存储区域根目录下的文件。它对应Environment.getExternalStorageDirectory返回的路径:eg:"/storage/emulated/0";

<external-files-path name="name" path="path" />

该方式提供在应用的外部存储区根目录的下的文件。它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路径。eg:"/storage/emulated/0/Android/data/com.xx.xxx/files"。

<external-cache-path name="name" path="path" />

该方式提供在应用的外部缓存区根目录的文件。它对应Context.getExternalCacheDir()返回的路径。eg:"/storage/emulated/0/Android/data/com.xx.xxx/cache"


4. 获取以content开头的文件拿不到正确路径

java.lang.IllegalArgumentException: column '_data' does not exist

拿到uri之后进行版本判断大于等于24(即Android7.0)用最新的获取路径方式

Stringstr ="";

if(Build.VERSION.SDK_INT >=24) {   

    str = getFilePathFromURI(this, uri);//新的方式

}else{   

     str = getPath(this, uri);你自己之前的获取方法

}

public String getFilePathFromURI(Context context, Uri contentUri) {

File rootDataDir = context.getFilesDir();

String fileName =getFileName(contentUri);

if (!TextUtils.isEmpty(fileName)) {

File copyFile =new File(rootDataDir + File.separator + fileName);

copyFile(context, contentUri, copyFile);

return copyFile.getAbsolutePath();

}

return null;

}

public static String getFileName(Uri uri) {

if (uri ==null)return null;

String fileName =null;

String path = uri.getPath();

int cut = path.lastIndexOf('/');

if (cut != -1) {

fileName = path.substring(cut +1);

}

return fileName;

}

public void copyFile(Context context, Uri srcUri, File dstFile) {

try {

InputStream inputStream = context.getContentResolver().openInputStream(srcUri);

if (inputStream ==null)return;

OutputStream outputStream =new FileOutputStream(dstFile);

copyStream(inputStream, outputStream);

inputStream.close();

outputStream.close();

}catch (Exception e) {

e.printStackTrace();

}

}

public int copyStream(InputStream input, OutputStream output)throws Exception, IOException {

final int BUFFER_SIZE =1024 *2;

byte[] buffer =new byte[BUFFER_SIZE];

BufferedInputStream in =new BufferedInputStream(input, BUFFER_SIZE);

BufferedOutputStream out =new BufferedOutputStream(output, BUFFER_SIZE);

int count =0, n =0;

try {

while ((n = in.read(buffer,0, BUFFER_SIZE)) != -1) {

out.write(buffer,0, n);

count += n;

}

out.flush();

}finally {

try {

out.close();

}catch (IOException e) {

}

try {

in.close();

}catch (IOException e) {

}

}

return count;

}

5、 7.0的手机安装没问题,但是在8.0上安装,app没有反应,一闪而过

增加新权限

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

Intent intent = new Intent(Intent.ACTION_VIEW)

改为

Intent intent = new Intent (Intent.ACTION_INSTALL_PACKAGE);


6. 解析包安装失败。

安装时把intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 这句话放在 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)前面。

File apkfile : 这个是你的apk文件地址对象。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

            Uri apkUri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", apkfile);  // 这个地方 关键

            Intent install = new Intent(Intent.ACTION_VIEW);

            install.setDataAndType(apkUri, "application/vnd.android.package-archive");

            install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件

            mContext.startActivity(install);

        } else {

            Intent i = new Intent(Intent.ACTION_VIEW);

            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            i.setDataAndType(Uri.parse("file://" + apkfile.toString()),

                    "application/vnd.android.package-archive");

            mContext.startActivity(i);

        }

7. 通知栏不显示

在Application中创建渠道

@Override

protected void onCreate() {

super.onCreate();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

String channelId ="chat";

String channelName ="聊天消息";

int importance = NotificationManager.IMPORTANCE_HIGH;

createNotificationChannel(channelId, channelName, importance);

channelId ="subscribe";

channelName ="订阅消息";

importance = NotificationManager.IMPORTANCE_DEFAULT;

createNotificationChannel(channelId, channelName, importance);

}

}

@TargetApi(Build.VERSION_CODES.O)

private void createNotificationChannel(String channelId, String channelName,int importance) {

NotificationChannel channel =new NotificationChannel(channelId, channelName, importance);

NotificationManager notificationManager = (NotificationManager) getSystemService(

NOTIFICATION_SERVICE);

notificationManager.createNotificationChannel(channel);

}

根据渠道发送消息new NotificationCompat.Builder(this, channelName)

public void sendChatMsg(View view) {

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Notification notification =new NotificationCompat.Builder(this,"chat")

.setContentTitle("收到一条聊天消息")

.setContentText("聊天消息具体内容")

.setWhen(System.currentTimeMillis())

.setSmallIcon(R.drawable.icon)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))

.setAutoCancel(true)

.build();

manager.notify(1, notification);

}

public void sendSubscribeMsg(View view) {

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Notification notification =new NotificationCompat.Builder(this,"subscribe")

.setContentTitle("收到一条订阅消息")

.setContentText("订阅消息 具体内容")

.setWhen(System.currentTimeMillis())

.setSmallIcon(R.drawable.icon)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))

.setAutoCancel(true)

.build();

manager.notify(2, notification);

}

}


8. 悬浮窗适配

使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:

TYPE_PHONE

TYPE_PRIORITY_PHONE

TYPE_SYSTEM_ALERT

TYPE_SYSTEM_OVERLAY

TYPE_SYSTEM_ERROR

相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。

也就是说需要在之前的基础上判断一下:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {   

mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

}  else{   

mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

}

需要新增权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

9. BroadcastReceiver无法接收广播

在Android 8之后需要setComponent才能收到广播消息,其中,ComponentName接收两个参数,参数1是自定义广播的包名,参数2是自定义广播的路径。

Intent intent = new Intent(action);

intent.putExtra(INTENT_DATA_PUSH, data);intent.addCategory(context.getPackageName());

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {

intent.setComponent(new ComponentName(context.getPackageName(), com.xx.broadcastReceiver);

}

context.sendBroadcast(intent);

二、Android 版本 9适配 -- targetSdkVersion 升级成28 需要注意的一些坑


1. 限制 HTTP 网络请求(*)

Android 9.0 中限制了 HTTP(明文传输)网络请求,若仍继续使用HTTP请求,则会在日志中提示以下异常(只是无法正常发出请求,不会导致应用崩溃):

java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy

适配的方法如下:

第一种

在资源目录中新建一个 xml 文件作为网络安全配置文件,例如 xml/network_security_config.xml,然后在文件中填写以下内容:

<?xml version="1.0" encoding="utf-8"?>

<network-security-config>

 <base-config cleartextTrafficPermitted="true">

<trust-anchors>

<certificates src="system" overridePins="true" />

<certificates src="user" overridePins="true" /> 

  </trust-anchors>

 </base-config> 

</network-security-config>

在AndroidManifest.xml进行配置:

<application    

...    android:networkSecurityConfig="@xml/network_security_config">    ...

</application>

第二种

Android 6.0 中引入了是否允许网络使用明文传输的配置:

<application android:usesCleartextTraffic=["true" | "false"]>

原来默认为 true,但在 Android 9.0 中默认值改为了 false,因此将配置手动设为 true 即可解决明文传输被限制的问题

2. 弃用 Apache HTTP Client

由于官方在 Android 9.0 中移除了所有 Apache HTTP Client 相关的类,因此我们的应用或是一些第三方库如果使用了这些类,就会抛出找不到类的异常:

java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/conn/scheme/SchemeRegistry;

若需要继续使用 Apache HTTP Client ,可通过以下方法进行适配:

在 AndroidManifest.xml 中添加以下内容:

<uses-library android:name="org.apache.http.legacy" android:required="false"/>

或者在应用中直接将 Apache HTTP Client 相关的类打包并进行引用

3. 限制非 SDK 接口的调用(*)

3.1 简述

一直以来,官方提供的接口分为了 SDK 接口和非 SDK 接口。SDK 接口即官方支持的接口,开发者可以直接调用不会有任何限制。一般而言,SDK 接口都记录在官方的接口索引中,没有记录的就视为非 SDK 接口,例如一些使用了@hide标注的方法。

以往开发者对于非 SDK 接口的调用通常是利用反射或者JNI间接调用的方式进行,但这样的调用方式如果处理不当会比较容易出现一些未知的错误。为了提升用户体验和降低应用发生崩溃的风险,Android 9.0 对应用能使用的非 SDK 接口实施了限制,具体的限制手段请见下表:


此外,为了开发者能够顺利过渡到 Android 9.0,官方对非 SDK 接口进行了分类,共分为三类,light-greylist(浅灰名单)、dark-greylist(深灰名单)以及blacklist(黑名单):

light-greylist(浅灰名单):对于此名单中的非 SDK 接口,官方暂未找到可替代的 SDK 接口,因此开发者仍可继续访问(如果 targetSdkVersion 大于等于28时会出现警告)。

dark-greylist(深灰名单):targetSdkVersion 小于28时仍可继续使用此名单中的接口,但会出现警告提示;大于等于28时,这些接口将会限制访问。

blacklist(黑名单):无论 targetSdkVersion 为多少,只要应用运行在 Android 9.0 平台上,访问此名单中的接口都会受限

3.2 如何测试应用是否使用非 SDK 接口

可以通过以下方式进行测试(详情请至官方文档):

使用 Android 9.0 或更高版本的设备调试应用

使用 StrictMode API 进行测试

使用 veridex 工具对应用进行扫描

建议使用第三种方式,该工具的扫描结果会列出应用对于三个限制名单中的接口的调用细节。

4. 前台服务权限

在 Android 9.0 中,应用在使用前台服务之前必须先申请FOREGROUND_SERVICE权限,否则就会抛出 SecurityException 异常。

此外,由于FOREGROUND_SERVICE权限只是普通权限,因此开发者只需在 AndroidManifest.xml 中注册此权限即可,系统会自动对此权限进行授权:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />


5. 强制执行 FLAG_ACTIVITY_NEW_TASK 要求(*)

在 Android 7.0(API 级别 24)之前,若开发者需要通过非 Activity context 启动 Activity,就必须设置 Intent 标志FLAG_ACTIVITY_NEW_TASK,否则会启动失败并抛出以下异常

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

但这个要求在更新 Android 7.0 以后由于系统问题被临时取消了,开发者即使不设置标志也可以正常启动 Activity。而在 Android 9.0 中官方修复了这个问题,这个要求重新开始强制执行,因此开发者在适配 Android 9.0 时需要注意这个问题。

6. 不允许共享 WebView 数据目录

Android 9.0 中为了改善应用稳定性和数据完整性,应用无法再让多个进程共用同一 WebView 数据目录。此类数据目录一般存储 Cookie、HTTP 缓存以及其他与网络浏览有关的持久性和临时性存储。

如果开发者需要在多进程中使用 WebView,则必须先调用WebView.setDataDirectorySuffix()方法为每个进程设置用于存储 WebView 数据的目录。若多进程 WebView 之间需要共享数据,开发者需自己通过 IPC 的方式实现。

此外,若开发者只想在一个进程中使用 WebView,并且希望严格执行这个规则,可以通过在其他进程中调用WebView.disableWebView()方法,这样其他进程创建 WebView 实例就会抛出异常

7. 其他 API 方面的修改

7.1 Region.Op 相关

Android 9.0 中如果在使用绘图裁剪功能时设置了除Region.Op.INTERSECT或Region.Op.DIFFERENCE以外的类型,就会抛出以下异常:

java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

具体原因是官方废弃了那几个具有Region.Op参数的裁剪方法,如clipRect(@NonNull RectF rect, @NonNull Region.Op op):

@Deprecated

public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op) {

checkValidClipOp(op);

return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,

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");

}

}

对于这个问题,可以通过以下方法进行适配:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

    canvas.clipPath(path);

} else {

    canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等类型

}

7.2 Build.SERIAL 被弃用(*)

Android 9.0 之前,开发者可以使用Build.SERIAL获取设备的序列号。现在这个方法被弃用了,Build.SERIAL将始终设置为 "UNKNOWN" 以保护用户的隐私。

适配的方法为先请求READ_PHONE_STATE权限,然后调用Build.getSerial()方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容