Android在版本迭代中,总会进行很多改动,下面我们一起梳理下。
Android 4.4
- 发布ART虚拟机,提供开启选项
- HttpURLConnection的底层实现替换成OkHttp
Android 5.0
- ART成为默认虚拟机,完全替代Dalvik虚拟机
Context.bindService()
方法需要显式 Intent,如果提供隐式 intent,将引发异常。
Android 6.0
- 增加运行时权限限制
如果你的应用使用到了危险权限,比如在运行时进行检查和请求权限。checkSelfPermission()
方法用于检查权限,requestPermissions()
方法用于请求权限。
- 取消支持Apache HTTP
Android 6.0 版移除了对 Apache HTTP
相关类库的支持。要继续使用 Apache HTTP API,您必须先在 build.gradle 文件中声明以下编译时依赖项:
android {useLibrary 'org.apache.http.legacy'}
Android 7.0
- Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2
- Toast导致的BadTokenException
- 在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现
FileUriExposedException
异常,如调用系统相机拍照录制视频,或裁切照片。
这一点其实就是限制了在应用间共享文件,如果需要在应用间共享,需要授予要访问的URI临时访问权限,我们要做的就是注册FileProvider
:
1.声明FileProvider。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="app的包名.fileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!--androidx版本类路径为:androidx.core.content.FileProvider-->
2.编写xml文件,确定可访问的目录
<paths xmlns:android="http://schemas.android.com/apk/res/android">
//代表设备的根目录new File("/");
<root-path name="root" path="." />
//context.getFilesDir()
<files-path name="files" path="." />
//context.getCacheDir()
<cache-path name="cache" path="." />
//Environment.getExternalStorageDirectory()
<external-path name="external" path="." />
//context.getExternalFilesDirs()
<external-files-path name="name" path="path" />
//getExternalCacheDirs()
<external-cache-path name="name" path="path" />
</paths>
3.使用FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri uri = FileProvider.getUriForFile(CameraActivity.this, "app的包名.fileProvider", photoFile);
} else {
Uri uri = Uri.fromFile(photoFile);
}
Android 8.0
- 修改运行时权限错误
在 Android 8.0
之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。对于针对 Android 8.0 的应用,系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。也就是说,以前你申请了READ_EXTERNAL_STORAGE
权限,应用会同时给你授予同权限组的WRITE_EXTERNAL_STORAGE
权限。如果Android8.0以上,只会给你授予你请求的READ_EXTERNAL_STORAGE
权限。如果需要WRITE_EXTERNAL_STORAGE
权限,还要单独申请,不过系统会立即授予,不会提示。
- 修改通知
Android 8.0 对于通知修改了很多,比如通知渠道、通知标志、通知超时、背景颜色。其中比较重要的就是通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。这样的好处就是对于某个应用可以把权限分成很多类,用户来控制是否显示哪些类别的通知。而开发者要做的就是必须设置这个渠道id,否则通知可能会失效。
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//分组(可选)
//groupId要唯一
String groupId = "group_001";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");
//创建group
notificationManager.createNotificationChannelGroup(group);
//channelId要唯一
String channelId = "channel_001";
NotificationChannel adChannel = new NotificationChannel(channelId,
"推广信息", NotificationManager.IMPORTANCE_DEFAULT);
//补充channel的含义(可选)
adChannel.setDescription("推广信息");
//将渠道添加进组(先创建组才能添加)
adChannel.setGroup(groupId);
//创建channel
notificationManager.createNotificationChannel(adChannel);
//创建通知时,标记你的渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("一条新通知")
.setContentText("这是一条测试消息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
}
- 悬浮窗
Android8.0以上必须使用新的窗口类型(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
}
- 不允许安装未知来源的应用
Android 8.0去除了“允许未知来源”选项,所以如果我们的App有安装App的功能(检查更新之类的),那么会无法正常安装。
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
private void installAPK(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, 100);
}
}else {
//安装应用
}
}
//接收“安装未知应用”权限的开启结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100) {
installAPK();
}
}
- Only fullscreen opaque activities can request orientation
只有全屏不透明的activity
才可以设置方向。这应该是个bug,在Android8.0中出现,8.1中被修复。我们的处理办法就是要么去掉设置方向的代码,要么舍弃透明效果。
Android 9.0
- 在9.0中默认情况下启用网络传输层安全协议 (TLS),默认情况下已停用明文支持。也就是不允许使用http请求,要求使用
https
。解决办法就是添加网络安全配置:
<application android:networkSecurityConfig="@xml/network_security_config">
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
<!--或者在AndroidManifest.xml中配置:
android:usesCleartextTraffic="true"
-->
- 移除Apache HTTP 客户端
在6.0中取消了对Apache HTTP
客户端的支持,Android9.0中直接移除了该库,要使用的话需要添加配置:
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
- 前台服务调用
Android 9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限,否则系统会引发 SecurityException。
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intentService);
} else {
startService(intentService);
}
- 不能在非Acitivity环境中启动Activity
在9.0 中,不能直接非 Activity 环境中(比如Service,Application)启动 Activity,否则会崩溃报错,解决办法就是加上FLAG_ACTIVITY_NEW_TASK
Intent intent = new Intent(this, TestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Android 10
- 分区存储
Android10中默认开启了分区存储,也就是沙盒模式。应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir()
访问)以及特定类型的媒体。如果需要关闭这个功能可以配置:
android:requestLegacyExternalStorage="true"
分区存储下,访问文件的方法:
1.应用专属目录
//分区存储空间
val file = File(context.filesDir, filename)
//应用专属外部存储空间
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)
2.访问公共媒体目录文件
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()
}
3.SAF(存储访问框架--Storage Access Framework)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, 100)
@RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data == null || resultCode != Activity.RESULT_OK) return
if (requestCode == 100) {
val uri = data.data
println("image uri is $uri")
}
}
- 权限再次升级
从Android10开始普通应用不再允许请求权限android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android Q(即targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。
如果Android10以下设备获取设备IMEI等信息,可以配置最大sdk版本:
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="28"/>
Android 11
- 分区存储强制执行
没错,Android11强制执行分区存储,也就是沙盒模式。在Android10的时候,Google还是为开发者考虑,留了一手。在targetSdkVersion = 29
应用中,设置android:requestLegacyExternalStorage="true"
,就可以不启动分区存储,让以前的文件读取正常使用。但是targetSdkVersion = 30
中不行了,强制开启分区存储。当然,作为人性化的Android,还是为开发者留了一小手,如果是覆盖安装呢,可以增加android:preserveLegacyExternalStorage="true"
,暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要卸载重装
,就会失效了。离Android11出来也有一段时间了,还是抓紧适配把。
- 媒体文件访问权限
为了在保证用户隐私的同时可以更轻松地访问媒体,Android 11 增加了以下功能。执行批量操作,使用直接文件路径和原生库访问文件。
1.执行批量操作
这里的批量操作指的是Android 11 向 MediaStore API
中添加了多种方法,用于简化特定媒体文件更改流程(例如在原位置编辑照片),分别是:
createWriteRequest()
用户向应用授予对指定媒体文件组的写入访问权限的请求。createFavoriteRequest()
用户将设备上指定的媒体文件标记为“收藏”的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为“收藏”。createTrashRequest()
用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容会在系统定义的时间段后被永久删除。createDeleteRequest()
用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求。
举个栗子:
val urisToModify = listOf(uri,uri,...)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
urisToModify)
// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
null, 0, 0, 0)
override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent?) {
when (requestCode) {
EDIT_REQUEST_CODE ->
if (resultCode == Activity.RESULT_OK) {
/* Edit request granted; proceed. */
} else {
/* Edit request not granted; explain to the user. */
}
}
}
传入uri的集合,获取用户的同意后,就可以进行操作了。
2.直接文件路径和原生库访问文件
没错!Android11又恢复了使用直接文件路径
访问媒体文件!哈哈,这样就方便多了。也就是除了 MediaStore API
之外还有两种方式可以访问媒体文件:
- File API
- 原生库,例如 fopen()
- 所有文件访问权限
MANAGE_EXTERNAL_STORAGE
,这个权限就是用来获取所有文件
的管理权限。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
val intent = Intent()
intent.action= Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
startActivity(intent)
//判断是否获取MANAGE_EXTERNAL_STORAGE权限:
val isHasStoragePermission= Environment.isExternalStorageManager()
- 电话号码相关权限
改动了两个API:getLine1Number()和 getMsisdn() ,需要加上READ_PHONE_NUMBERS权限
- 不允许自定义toast从后台显示了,如果实在需要后台显示,就用普通的toast吧!
- 必须加上v2签名
- 多媒体Intent操作,只有预装的系统相机可以响应
以下 intent 操作:
android.media.action.VIDEO_CAPTURE
android.media.action.IMAGE_CAPTURE
android.media.action.IMAGE_CAPTURE_SECURE
也就是说,如果我调用intent
唤起照相机,使用VIDEO_CAPTURE
的action,只有系统的相机能够响应,而第三方的相机应用不会响应了。官方给的建议是如果要使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为intent
设置软件包名称或组件来使这些intent变得明确。
- 增加5g相关API
1.检测是否连接到了5G网络
通过TelephonyManager
的监听方法:
private fun getNetworkType(){
val tManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
tManager.listen(object : PhoneStateListener() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {
if (ActivityCompat.checkSelfPermission(this@Android11Test2Activity, android.Manifest.permission.READ_PHONE_STATE) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
return
}
super.onDisplayInfoChanged(telephonyDisplayInfo)
when(telephonyDisplayInfo.networkType) {
TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO -> showToast("高级专业版 LTE (5Ge)")
TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> showToast("NR (5G) - 5G Sub-6 网络")
TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> showToast("5G+/5G UW - 5G mmWave 网络")
else -> showToast("other")
}
}
}, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)
}
2.检查按流量计费性
监听网络,在回调中判断:
val manager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
//true 代表连接不按流量计费
val isNotFlowPay=networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ||
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
}
})
- 后台位置访问权限再次限制
1.从Android10系统的设备开始,就需要请求后台位置权限(ACCESS_BACKGROUND_LOCATION)
,并选择Allow all the time (始终允许)
才能获得后台位置权限。Android11设备上再次加强
对后台权限的管理,主要表现在系统对话框
上,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要在设置页面选择始终允许
才能获得后台位置权限。
2.在搭载Android11系统
的设备上,targetVersion小于30的时候,可以前台后台位置权限一起申请,并且对话框提供了文字说明,表示需要随时获取用户位置信息,进入设置选择始终允许
即可。但是targetVersion为30的时候,你必须单独申请
后台位置权限,而且要在获取前台权限之后,顺序不能乱
。并且无任何提示,需要开发者自己设计提示样式。
所以,该怎么适配呢?
1.targetSdkVersion<30
情况下,如果你之前就有判断过前台和后台位置权限,那就无需担心,没有什么需要适配。
2.targetSdkVersion>=30
情况下,需要分开申请前后台位置权限,并且对后台位置权限申请做好说明和引导,当然也是为了更好的服务用户。
权限申请的demo代码:
val permissionAccessCoarseLocationApproved = ActivityCompat
.checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (permissionAccessCoarseLocationApproved) {
val backgroundLocationPermissionApproved = ActivityCompat
.checkSelfPermission(this, permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (backgroundLocationPermissionApproved) {
//前后台位置权限都有
} else {
//申请后台权限
if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
200)
}else{
AlertDialog.Builder(this).setMessage("需要提供后台位置权限,请在设置页面选择始终允许")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialog, which ->
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
200)
}).create().show()
}
}
} else {
if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
//申请前台和后台位置权限
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION),
100)
}else{
//申请前台位置权限
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
100)
}
}
- 软件包可见性
Android11中
,如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须在清单文件中添加<queries>
元素,告知系统你要获取哪些应用信息或者哪一类应用。此外,此元素还可帮助 Google Play 等应用商店评估应用为用户提供的隐私权和安全性。
比如我这段查询应用信息的代码:
val pm = this.packageManager
val listAppcations: List<ApplicationInfo> = pm
.getInstalledApplications(PackageManager.GET_META_DATA)
for (app in listAppcations) {
Log.e("lz",app.packageName)
}
- 自动重置权限
如果应用以 Android 11 为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的运行时敏感权限来保护用户数据。此操作与用户在系统设置中查看权限并将应用的访问权限级别更改为拒绝的做法效果一样。如果应用已遵循有关在运行时请求权限的最佳做法,那么您不必对应用进行任何更改。这是因为,当用户与应用中的功能互动时,您应该会验证相关功能是否具有所需权限。说的很清楚了,只要应用遵循有关在运行时请求权限的最佳做法,也就是每次需要调用权限
的时候都会去判断,那么就不会有什么问题。
如果需要关闭这个功能怎么办呢?只有引导用户去设置页面关闭了,可以调用包含Settings.ACTION_APPLICATION_DETAILS_SETTINGS action
的 Intent将用户定向到系统设置中应用的页面。
怎么检查应用是否停用自动重置功能呢?调用 PackageManager的isAutoRevokeWhitelisted()
方法。如果此方法返回 true,代表系统不会自动重置应用的权限。
总结
行动吧~~~
参考:https://developer.android.google.cn/about/versions/11 https://juejin.cn/post/6860370635664261128#heading-29