Android 8.0 targetsdkversion升级到26填坑
目录
前言
1、动态权限管理
2、ContentResolver
3、FileProvider(File URI)
4、DownloadManager(ContentResolver.openFileDescriptor)
5、后台service
6、集合API变更
7、通知Notification
8、隐式广播
9、悬浮窗
前言
近期因为应用市场要求,需要将targetsdkversion升级到26
之前博客中我们了解过targetsdkversion的重要性,当时我们建议轻易不要改动这个参数。
但是这次因为应用市场的硬性要求,我们必须做升级,那么就需要面对升级后带来的兼容性问题。
1、动态权限管理
最明显的问题就是权限管理,在6.0加入的动态权限需要我们手动进行处理。这个就老生常谈了,这里不展开说了。
2、ContentResolver
处理完权限我们运行程序后,发现app竟然crash了,报错:
java.lang.SecurityException: Failed to find provider xxx for user 0; expected to find a valid ContentProvider for this authority
调查发现我们使用了
getContentResolver().registerContentObserver(uri, false, observer)
或
getContentResolver().notifyChange(uri, observer)
查询相关文档得知8.0对ContentResolver加了一层安全机制,防止外部访问app内部使用的数据。
那么怎么解决这个问题?
首先我们需要自定义一个ContentProvider,如果仅仅是为了通知,可以不实现抽象函数
然后在AndroidManifest中
<providerandroid:authorities="xxxx"android:name=".ContentProvider"/>
最后在使用时uri需要是content://<authorities>/...形式,如
getContentResolver().registerContentObserver(new URI().parse("content://xxxx/tablename"), false, observer)
getContentResolver().notifyChange(new URI().parse("content://xxxx/tablename"), null)
3、FileProvider(File URI)
在8.0(实际是7.0)下,当app对外传递file URI的时候会导致一个FileUriExposedException。
比如说在app拉起安装apk,之前代码是:
Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(Uri.parse(file), "application/vnd.android.package-archive");
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
当tagsdkversion升级到26就会出现问题。这是因为7.0添加了一项安全机制,app不再允许对外暴露File URI(file://URI),而用Content URI(content://URI)来代替,Content URI会授予URI临时访问权限,提高文件访问的安全性。
那么如何使用Content URI?
添加FileProvider即可,它是ContentProvider的一个子类。
首先,在res的xml目录(没有则新建)下,新增一个文件file_provider_paths.xml:
<?xml version="1.0" encoding="utf-8"?>
<paths><external-pathname="apkDownload"path="download"/>
</paths>
这里注意paths一定要小写,大写也不会报错,但是会造成一些麻烦;再有path不能为空。
这里就要详细解释一下:
- name是Content URI对外暴露的伪路径,它对应这path
- path则是真实路径
- external-path则表示sd卡,这里几种选择,如下:
- files-path对应Context.getFilesDir() + “/path/”,即/data/data/<package-name>/files/path/。
- cache-path对应Context.getCacheDir() + “/path/”,即/data/data/<package-name>/cache/path/。
- external-files-path对应Context.getExternalFilesDir(null) + “/path/”,即/storage/emulated/0/Android/data/<package_name>/files/path/。
- external-cache-path对应Context.getExternalCacheDir() + “/path/”,即/storage/emulated/0/Android/data/<package-name>/cache/path/。
- external-path对应Environment.getExternalStorageDirectory() + “/path/”,即
/storage/emulated/0/path/
。
所以在使用要格外注意,一定要与真实地址对应上。比如真实地址为/data/data/<package-name>/cache/apk/1.apk
那么就是:
<cache-pathname="apkDownload"path="apk"/>
而最终得到的Content URI则是content://<authorities>/apkDownload/1.apk
可以看到使用authorities(在后面)和name隐藏了真实路径,这样就防止对外暴露了路径
其次,在AndroidManifest中添加:
<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" />
</provider>
exported和grantUriPermissions都不能缺,否则会引起错误
这里的authorities就是最后Content URI中的,而且后面还会使用到
然后,我需要重写拉起安装的代码,如下:
Intent i = new Intent(Intent.ACTION_VIEW);
Uri contentUri;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);contentUri = FileProvider.getUriForFile(context, authorities, file);
}
else{contentUri = Uri.parse(file);
}
i.setDataAndType(contentUri, "application/vnd.android.package-archive");
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
注意这里的authorities一定要与AndroidManifest中的保持一致。
最后再记录一下开发过程中遇到的几个问题:
(1)InstallStart: Requesting uid 10087 needs to declare permission android.permission.REQUEST_INSTALL_PACKAGES
在8.0以上,需要在AndroidManifest中添加<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>权限
(2)安装包解析失败
这里有两种情况,可以根据日志分析出来:
(1)路径错误:日志中有No such file or directory这样的字眼。检查文件路径和上面的配置是否有误,比如文件在sd卡而xml中是应用cache中。
(2)权限问题,日志如下:
W/System.err: java.lang.SecurityException: Permission Denial: opening provider ... from ProcessRecord ... that is not exported from .
W/PackageInstaller: InstallStaging:Error staging apk from content URIPermission Denial: opening provider ... from ProcessRecord ... that is not exported from ...
网上对有不少说法:不能使用sd卡,必须用应用空间;还有将android:exported设为true。事实证明都不对,尤其android:exported设为true会造成java.lang.SecurityException: Provider must not be exported错误。
这个问题实际上是没有给intent添加Intent.FLAG_GRANT_READ_URI_PERMISSION这个flag。尤其要注意,因为这里要添加两个flag,一定要使用addFlags。如果使用setFlags,由于Intent.FLAG_ACTIVITY_NEW_TASK在后面设置,会丢失前面的flag,会导致上面的问题。
(3)FileProvider冲突
当lib或module中的AndroidManifest添加了FileProvider,而主项目也需要添加时,就会出现冲突,gradle编译错误如下:
Error:C:***AndroidManifest.xml:352:13-62 Error:
Attribute provider#android.support.v4.content.FileProvider@authorities value=(***.fileProvider) from AndroidManifest.xml:352:13-62
is also present at [xxxx:xxx] AndroidManifest.xml:19:13-64 value=(***.fileprovider).
Suggestion: add 'tools:replace="android:authorities"' to <provider> element at AndroidManifest.xml:350:9-358:20 to override.
错误上建议我们添加tools:replace="android:authorities"来解决问题,实际上我发现这并不能很好的解决问题。那么怎么办?
简单的方法是我们自定义一个类,继承FileProvider,在AndroidManifest中使用这个自定义类,这样就可以避免冲突了。如:
<providerandroid:name=".MyFileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" />
</provider>
4、DownloadManager(ContentResolver.openFileDescriptor)
同样在7.0上,为了安全起见对DownloadManager也做了修改,抛弃了COLUMN_LOCAL_FILENAME字段。
在之前我们使用DownloadManager,会使用一个receiver来接收下载结束并处理后续,代码如下:
@Override
public void onReceive(Context context, Intent intent) {DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);if (downloadId == mId) {DownloadManager.Query query = new DownloadManager.Query();query.setFilterById(mId);Cursor cursor = downloadManager.query(query);if (cursor.moveToFirst()) {int urlId = cursor.getColumnIndex(DownloadManager.COLUMN_URI);int stateId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);String url = cursor.getString(urlId);int state = cursor.getInt(stateId);int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);tmp = cursor.getString(pathId);...
可以看到从cursor里可以得到下载状态,url及下载地址等信息。
但是当targetsdkversion升级到7.0或以上后,在7.0及以上机器上就会crash,报错如下:
java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
如上所说,为了安全起见抛弃了COLUMN_LOCAL_FILENAME(还有COLUMN_LOCAL_URI字段),让我们用ContentResolver.openFileDescriptor()代替。
那么ContentResolver.openFileDescriptor()又是什么?
简单来说会得到一个ParcelFileDescriptor对象,并可以进一步得到一个FileDescriptor对象。
我们可以通过它们打开文件流,比如:
int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
String localUri = cursor.getString(pathId);
try {ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(Uri.parse(localUri), "r");FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();FileInputStream fileInputStream = new FileInputStream(fileDescriptor);...
} catch (FileNotFoundException e) {e.printStackTrace();
}
(使用FileDescriptor还有其它方法,都是通过流来处理)
那么我们如果只需要下载文件的完整路径即可,这时就不需要openFileDescriptor,只需要将上面获取下载文件的两行代码替换为:
String tmp = null;
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M){int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);String localUri = cursor.getString(pathId);tmp = Uri.parse(localUri).getPath();
}
else{int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);tmp = cursor.getString(pathId);
}
在7.0之上处理一下就可以了。
5、后台service
这里有两点需要注意:
(1)当app在后台时,startService启动一个background service将不再允许,会导致crash。bindService不受影响。
解决方法是避免app不在前端时启动服务;或者如果一定需要app在后台启动服务,请启动一个foreground service,但是需要一个常驻的notifacation; 或者使用bindService来启动服务。具体做法有很多文章,这里就不详细写了。
(2)service的存活差异
经测试发现,targetsdkversion的改变对service(background service)的存活也是有影响的。
这块我有一篇详细的文章来讲解,请见《探讨8.0版本下后台service存活机制及保活》
对于targetsdkversion 26的app在8.0及以上版本,想要长时间存活,最好的方式就是使用foreground service;或者将service绑定到application上bindService;另外一个解决方案就是请求加入耗电白名单,但是这个对用户不友好。
6、集合API变更
在android8.0上AbstractCollection.removeAll(null)和AbstractCollection.retainAll(null)会引发NullPointerException;之前版本则不会。所以我们在使用这两个函数前要确保参数不是null,必要是需要判空。
可以看看这两个函数的代码:
public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<?> it = iterator();while (it.hasNext()) {if (c.contains(it.next())) {it.remove();modified = true;}}return modified;
}
public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<E> it = iterator();while (it.hasNext()) {if (!c.contains(it.next())) {it.remove();modified = true;}}return modified;
}
可以看到都在首行调用了Objects.requireNonNull(c),这个代码是:
public static <T> T requireNonNull(T obj) {if (obj == null)throw new NullPointerException();return obj;
}
可以看到如果是null就会抛出一个空指针错误。
7、通知Notification
在Android8.0上,通知做了较大的改动,增加了分组和渠道机制,这样更加方便了用户对通知的管理。
那么我们app中的notification相关代码就需要变动,否则可以无法发出通知。
我们需要为notification增加分组和渠道,如下:
String groupId = "group1";NotificationChannelGroup group = new NotificationChannelGroup(groupId, "");notificationManager.createNotificationChannelGroup(group);String channelId = "channel1";NotificationChannel channel = new NotificationChannel(channelId,"推广信息", NotificationManager.IMPORTANCE_DEFAULT);channel.setDescription("推广信息");channel.setGroup(groupId);notificationManager.createNotificationChannel(channel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId);
//上面使用support包中的NotificationCompat,但是版本需要是26及以上
//或者不使用support包中的类,直接使用Notification.Builder,但是要进行版本判断//Notification.Builder builder;
//if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// builder = new Notification.Builder(BaseApp.getAppContext(), App.CHANNEL_ID);
//}
//else{
// builder = new Notification.Builder(BaseApp.getAppContext());
//}...
notificationManager(notificationId, builder.build())
这样通知就能正常发出并展示了,但是其实group并不是必须的,所以可以只设置channel,如:
String channelId = "channel1";NotificationChannel adChannel = new NotificationChannel(channelId,"推广信息", NotificationManager.IMPORTANCE_DEFAULT);adChannel.setDescription("推广信息");notificationManager.createNotificationChannel(adChannel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId);
...
notificationManager(notificationId, builder.build())
8、隐式广播
android8.0之后静态注册(manifest中)的隐式广播将不再起作用,但是有一些隐式广播除外,静态注册它们仍然可以接收到广播。
解决方法是将隐式广播改成动态注册。
静态注册的显式广播不受影响
9、悬浮窗
使用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>=26) {windowParams.type= WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}else{windowParams.type= WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
而且在manifest添加权限后,如果在6.0及以上系统中还需要动态请求相关权限:
if (Build.VERSION.SDK_INT >= 23) {if(!Settings.canDrawOverlays(getApplicationContext())) {Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,Uri.parse("package:" + getPackageName()));startActivityForResult(intent,1);return;} else {}
} else {}@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {if (requestCode == 1) {if (Build.VERSION.SDK_INT >= 23) {if (!Settings.canDrawOverlays(this)) {...}}}
}
Android 8.0 targetsdkversion升级到26填坑相关推荐
- android多语言插件,Android 7.0多语言适配,填坑日记(附多语言插件)
背景 最近项目在适配7.0系统的时候发现一些问题,其中也有多语言适配的问题,就拿出来说说,记记笔记,填填坑. 问题1 因为刚接手项目不久,发现一些奇奇怪怪的bug总是让人头疼,最近发现了在华为荣耀7. ...
- s6 android 6.0,三星Android 6.0更新升级详细名单:只有国行S6 Edge(G9250)
此前,外媒曾泄露了一份三星机型的Android 6.0首批升级清单,去年的旗舰机Galaxy S5竟不在其中,使得消费者对手机第一大厂很失望.其中,三星目前的旗舰智能手机Galaxy S6, Gala ...
- moto x android 6.0,MOTOXPRO升级安卓6.0
摩托罗拉安卓6.0升级名单 国行MOTO四月底全部更新完成 以下为MOTO支持升级安卓6.0的机型: 1.2015 Moto X Pure Edition (第三代) 2.2015 Moto X St ...
- Android Studio 插件开发详解四:填坑
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78265540 本文出自[赵彦军的博客] 系列目录 Android Gradle使用 ...
- Android Studio 3.0~3.x正式版填坑之路
序言 总看别人的文章,今天尝试着自己来写一篇.在逛论坛时候,无意间发现Android Studio 3.0正式版本推送更新了,早听说AS 3.0添加了许多新功能,然后手贱迫不及待地想先睹为快,结果正中 ...
- android 7.0独立升级,爆料:Android 7.0用户将可自行升级!
原标题:爆料:Android 7.0用户将可自行升级! 谷歌:大家好,谷歌武林大会(I/O大会)将在美西时间5月18-20日召开,届时激动人心的Android 7.0就要来啦. 手机厂商:我了个擦,老 ...
- 小米3升级android 6.0,可升级Android 6.0机型一览 小米手机亮了
前不久,Android M被谷歌正式确定为6.0系统,代号Marshmallow,预计年底前发布.而对于普通用户来说,最关心的就是自己的手机能不能升级.不过别着急,在此之前,我们不妨看看Android ...
- htc m7 android 6.0,HTC升级Android 6.0名单曝光 M7无缘或被淘汰
原标题:HTC升级Android 6.0名单曝光 M7无缘或被淘汰 腾讯数码讯(Eskimo)新一代Android 6.0 Marshmallow操作系统将在10月5日正式发布,而首先获得升级的自然是 ...
- android 7 版本升级,Android 7.0独立升级是什么意思?安卓7.0系统升级方法介绍[多图]...
类型:手机系统 大小:430.87MB 评分:9.6 平台: 标签: Android7.0的相关资讯已经来了,而且它的新功能也已经透露出来了,小伙伴们对这款系统还是比较期待的,但是发布时间我们可以暂时 ...
最新文章
- TCP全连接和半连接的问题探讨
- CentOS下防御或减轻DDoS攻击方法(转)
- SAP Analytics Cloud里的Planning model
- Oracle命令(二):Oracle数据库几种启动和关闭方式
- python写前端图形界面_如何Tkinter模块编写Python图形界面
- 简自动类型提升,精度损失类型强制转换,常用转义字符,简单帮你回顾Java基本数据类型整形浮点型字符型布尔型Boolean及其运算规则
- C++ deque
- Hls之TS流分离音视频
- PHP拼接SQL语句批量更新多个字段
- 磨皮ps教程-庞姿姿
- iOS框架引见--媒体层
- 实验02 使用网络模拟器Packet Tracer
- win10文件服务器ssd当缓存盘,Win10怎么启用设备上的写入缓存|提高固态硬盘性能方法...
- social域名是什么样的域名?有什么注册规则?
- php-fpm 多核,linux 多核CPU性能调优
- 陈老师排课12A版的手工调课的方法
- java iText 的jar导出pdf格式,禁止浏览器预览,直接下载功能
- 使用 Apple Watch S6 测量血氧教程
- php 除法向上取整,PHP - 除法取整数
- git 移除项目版本控制_Git - 关于版本控制
热门文章
- 一个高级的makefile文件
- 高创新出GoTVbox多路电视解调器
- cannot be found on object of type xx.CacheExpressionRootObject
- 第二章 Linux系统安装 - VMware虚拟机安装、卸载与使用
- iOS开发实战小知识点(五)——获取JS meta异常
- Linux学习之CentOS(五)--让我有些郁闷的mount命令
- [BZOJ 3894]文理分科(最小割)
- 常用的简单排序之插入排序,冒泡排序,选择排序,希尔排序
- windows XP上实现python2.7.5和python3.4.3共存
- powershell命令大全