一:介绍
我们可以看到Android版本对应的Api版本
二:Android 6.0 (API 23)
Google I/O 2015大会如约已于2015年5月28日举行。在发布会上代号为“Marshmallow(棉花糖)”的安卓6.0系统正式推出。
Android 6.0 的API级别:23
新特性:
1.运行时权限(最主要)
此版本引入了一种新的权限模式,用户可直接在运行时管理应用权限。
对于以 Android 6.0(API 级别 23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。要确定您的应用是否已被授予权限,请调用新增的 checkSelfPermission() 方法。要请求权限,请调用新增的 requestPermissions() 方法。即使您的应用并不以 Android 6.0(API 级别 23)为目标平台,您也应该在新权限模式下测试您的应用。
//检查权限是否允许
ContextCompat.checkSelfPermission
//请求某个或某几个权限
ActivityCompat.requestPermissions
//手动请求权限之后的结果回调
onRequestPermissionsResult
//是否显示权限请求弹窗
//第一次请求权限时ActivityCompat.shouldShowRequestPermissionRationale=false;
//第一次请求权限被禁止,但未选择【不再提醒】ActivityCompat.shouldShowRequestPermissionRationale=true;
//允许某权限后ActivityCompat.shouldShowRequestPermissionRationale=false;
//禁止权限,并选中【禁止后不再询问】ActivityCompat.shouldShowRequestPermissionRationale=false;
shouldShowRequestPermissionRationale
2.取消支持Apache HTTP客户端
Android 6.0版本移除了对Apache HTTP客户端的支持。
如果您的应用使用该客户端,并以 Android 2.3(API 级别 9)或更高版本为目标平台,请改用 HttpURLConnection 类。此 API 效率更高,因为它可以通过透明压缩和响应缓存减少网络使用,并可最大限度降低耗电量。要继续使用 Apache HTTP API,您必须先在 build.gradle 文件中声明以下编译时依赖项:
android {
useLibrary 'org.apache.http.legacy'//使用Apache库
}
三:Android 7.0 (API 24-25)
Android 7.0是Google推出的智能手机操作系统,官方代号为“Nougat”(牛轧糖)。于2016年5月18-20日(美国西部时间)在Google I/O开发者大会上正式发布,发布地点是山景城的Shoreline Ampitheatre圆形剧场
Android 7.0 包括旨在延长设备电池寿命和减少 RAM 使用的系统行为变更。这些变更可能会影响您的应用访问系统资源,以及您的应用通过特定隐式 intent 与其他应用交互的方式。
新特性:
1.低电耗模式
Android 6.0引入了低电耗模式,当用户设备未插接电源,处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。而 Android 7.0 则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。
2.系统权限的更改
为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问 (0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重副作用
传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
Android7.0之前访问系统相册
File file = new File(Environment.getExternalStorageDirectory(), "/temp/" + System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
Uri imageUri = Uri.fromFile(file);
Intent intent = new Intent();
//设置Action为拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
//将拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, 1);
在7.0之后的话访问相册会报如下错误:
android.os.FileUriExposedException: file:///storage/emulated/0/temp/1627010812423.jpg exposed beyond app through ClipData.Item.getUri()
在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片。
解决方法:
第一步:在清单文件AndroidManifest.xml中注册provider
<provider
android:name="androidx.core.content.FileProvider"//非androidx下 android:name="android.support.v4.content.FileProvider"
android:authorities="com.ruan.mygitignore.fileprovider"//包名.fileprovider
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
参数解释:
android:name:是固定的
android:authorities推荐写您的应用包名+“.fileprovider”,其实这里不一定要写fileprovider,您也可以随便写,只要与后面使用FileProvider.getUriForFile()这个方法中的第二个参数authority对应起来即可;(URI uri = FileProvider.getUriForFile(context, “com.ruan.mygitignore.fileprovider”, file);)
android:grantUriPermissions固定true,表示uri访问授权;
android:exported固定的false,我试着写了true报安全异常。
android:resource表示我们app要共享文件的路径的资源文件。
第二步:res文件夹下,新建一个xml文件夹,名字就是上一步 android:resource=”@xml/file_paths”对应的内容
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="Camera"
path="" />
</paths>
命名为“file_paths”(名字可以随便起,只要和第一步中在manifest注册的provider所引用的resource保持一致即可)的资源文件。
上述代码中 path=”“,是有特殊意义的,它指的是根目录,也就是说您可以向其它的应用访问根目录及其子目录下任何一个文件了,如果您将path设为 path=”pictures”,那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),这时您访问pictures目录范围之外的文件是不行的。
files-path代表的根目录: Context.getFilesDir()
external-path代表的根目录: Environment.getExternalStorageDirectory()
cache-path代表的根目录: getCacheDir()
第三步:使用FlieProvider
/*
*相册,选择一张图片
*/
private void getPhoto(Context context) {
// 这里用时间命名是发现用固定名命名后第二次裁剪图片任然是第一次的图,没有覆盖上一次图片资源;7.0之前固定名会替换
File file = new File(Environment.getExternalStorageDirectory(), "/temp/" + System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
// 通过FileProvider创建一个content类型的Uri
Uri imageUri = FileProvider.getUriForFile(context, "com.ruan.mygitignore.fileprovider", file);
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 设置Action为拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
// 将拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 开启一个带有返回值的Activity,请求码为PHOTO_REQUEST_GALLERY
startActivityForResult(intent, 1);
System.out.println(imageUri);
}
结果
System.out: content://com.ruan.mygitignore.fileprovider/Camera/temp/1627018704311.jpg
最简单的以打开相机为例:
File file = new File(Environment.getExternalStorageDirectory(), "/temp/" + System.currentTimeMillis() + ".jpg");
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (Build.VERSION.SDK_INT >= 24) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//24以上使用FileProvider
intent.putExtra(MediaStore.EXTRA_OUTPUT,
FileProvider.getUriForFile(FiveTeenActivity.this, "com.ruan.mygitignore.fileprovider", file));
}else{
//24以下
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
}
startActivityForResult(intent, 1);
1、将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri。
2、添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。
四:Android 8.0 (API 26-27)
2017年8月22日,谷歌正式发布了Android 8.0的正式版,其正式名称为:Android Oreo(奥利奥) 。2017年12月5日谷歌正式发布了Android 8.1的正式版。
新特性:
1.通知渠道 — Notification Channels
通知渠道是由应用自行定义的通知内容类别,借助渠道,开发者可以让用户对不同种类的通知进行精细控制,用户可以单独拦截或更改每个渠道的行为,而不是统一管理应用的所有通知。
创建通知渠道的步骤:
创建 NotificationChannel 对象,并设置应用内唯一的通知 ID。
配置通知渠道的属性,比如提示声音等。
在 NotificationManager 中注册通知渠道对象。
厂商推送
对接厂商推送时需要先注册渠道并引导用户开启渠道权限,不然第一条推送无法收到
- 通知消息:指定通知标题和内容后,由个推SDK自动处理在系统通知栏中展示通知栏消息,同时响铃或震动提醒用户(响铃和震动受手机系统的设置状态影响)。
- 透传消息:即自定义消息,消息体格式客户可以自己定义,如纯文本、json串等。透传消息个推只传递数据,不做任何处理,客户端接收到透传消息后需要自己去做后续动作处理,如通知栏展示、弹框等。各平台有不同处理(小米、魅族弃用;华为应用不在前台时,推送服务会加密缓存消息内容;荣耀、vivo、oppo不支持)
1.小米,通知总开关默认开启,通知渠道默认开启;
https://dev.mi.com/console/doc/detail?pId=2086
2.华为,通知总开关默认开启,通知渠道默认开启;
https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides-V5/message-classification-management-solution-0000001149358835-V5
- 消息分类
- 服务与通讯,包括社交通讯类消息和服务提醒类消息。
- 社交通讯,指用户间的聊天消息、通话等信息。
- 服务提醒,指应用借助通知中心及时向用户传递重要通知提醒,通常用户对接收此类消息有预期。
- 资讯营销,包括资讯类消息和营销类消息,指的是运营人员向用户发送的活动信息、内容推荐、资讯等。
- 消息分类方式有两种:
- 消息智能分类:智能分类算法将根据您发送的内容等多个维度因素,自动将您的消息按照分类标准进行归类。
- 消息自分类:即日起华为推送服务开始接收开发者自分类权益的申请。申请成功后,允许开发者根据华为推送分类规范,自行对消息进行分类。自分类权益上线日期:2021年07月01日
默认情况下,所有消息一律通过通知消息智能分类功能进行分类。如您希望消息分类能更精准地符合业务需要,您也可以申请自分类权益,我们将信任您所提供的分类信息,按照您提供的分类标准展示对应消息。
- 通知渠道(channel)是Android O版本引入的新功能,意在解决如下问题:
- 应用的通知越来越多,给用户造成明显打扰。
- 华为手机系统EMUI 10.0之前仅有一个“默认通知”渠道,无法做通知消息呈现方式的定制。
为了改善终端用户推送体验、营造良好可持续的通知生态,华为推送从EMUI
10.0对推送消息分类进行管理。根据消息内容,华为推送将通知分类为服务与通讯、资讯营销两大类别。华为手机系统从EMUI 10.0开始新增了两个通知渠道(普通通知、营销通知)来实现不同分类消息(服务与通讯、资讯营销)的通知消息展示。
- 自定义通知渠道,使用自定义渠道推送消息请参见如下步骤:
注意:自定义渠道功能不再适用于数据处理位置为中国区的应用,您的推送消息将按照智能分类系统或消息自分类权益确认的消息级别,归类为服务与通讯类或资讯营销类消息。
- 您应用的客户端创建自定义渠道,详情请参见Android官方文档
- 您的应用具有消息自分类权益,如果没有请参见模板申请自分类权益。
- 下行消息体内容中设置importance字段为NORMAL,channel_id字段设置为您自定义的渠道。消息体示例:
在这里插入代码片
3.荣耀,通知总开关默认开启,通知渠道默认开启;
https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=notification-class.md&token=
- 消息分类
荣耀推送服务将根据应用类型、消息内容和消息发送场景,将推送消息分成服务通讯和资讯营销两大类别。
服务通讯类,包括社交通讯消息和服务提醒消息。
- 社交通讯,指用户间的聊天消息、音视频通话。
- 服务提醒,指应用及时向用户传递重要通知提醒,通常用户对接收此类消息有预期。
资讯营销类,包括内容资讯消息和活动营销消息。
- 内容资讯,指应用向用户推送的推荐内容、资讯等。
- 活动营销,指应用向用户推送的产品促销、功能推荐、运营活动等。
- 消息分类方式
荣耀推送服务针对消息分类有2种处理方式:
- 消息智能分类:智能算法将根据APP类型和消息内容等维度,自动将您的消息按照分类标准进行归类。
- 消息自分类:允许开发者根据消息分类规范,自行对消息进行分类。
目前,所有消息默认通过消息自分类方式进行分类处理,荣耀推送服务将充分信任您提供的分类结果,并且按您提供的分类结果展示对应信息。随着荣耀推送服务能力的不断补充和演进,分类方式也会逐渐更新与升级,请及时留意本文档最新的分类方式说明。
- 消息自分类标准
要求
- 应用已经在荣耀开发者平台开通荣耀推送服务;
- 应用签署《荣耀推送服务使用协议》;
- 应用必须充分遵守消息分类标准,对消息进行归类处理。
应用适配开发
应用的推送消息将根据message.android.notification.importance字段进行归类。
- importance字段值为“LOW”时,表示消息为资讯营销类,默认展示方式为静默通知,仅在下拉通知栏展示;
- importance字段值为“NORMAL”时,表示消息为服务通讯类,默认展示方式为锁屏展示+下拉通知栏展示。
- 每日推送数量上限
4.vivo,通知总开关默认开启,通知渠道类别默认关闭;
发测试推送消息时不可纯数字、英文、特殊字符;不可带测试、test等明显的测试数据标识;运营消息可发送条数有上限
https://dev.vivo.com.cn/documentCenter/doc/359
5.oppo,通知总开关默认关闭,通知渠道类别默认开启;
https://open.oppomobile.com/new/developmentDoc/info?id=11227
2、画中画模式 — PIP
Android O 现已支持 Activity 的画中画模式。PIP 是一种多窗口显示模式,多用于视频播放。这和普通的画中画分屏模式并不相同。这一功能的唤醒只需要点击Home键按钮,如果想结束这一模式,可以将小窗口滑下来以终止。
关于生命周期
PIP 模式不会改变 Activity 的生命周期。在指定时间只有最近与用户交互过的 Activity 为活动状态。 该 Activity 将被视为顶级 Activity。 所有其他 Activity 虽然可见,但均处于暂停状态。当一个 Activity 处于 PIP 模式时,其实它是出在暂停状态,但其内容会继续展示。
API变更
在 Android O 中新增 PictureInPictureArgs 对象来指明你的 Activity 在 PIP 模式中的属性,比如长宽比等。
Android O 还新增了以下方法来支持 PIP。
Activity.enterPictureInPictureMode(PictureInPictureArgs args):将Activity置于 PIP 模式之下。
Activity.setPictureInPictureArgs():用于更新 Activity 在 PIP 模式下的设置。如果 Activity 正处于 PIP 模式之下,那么更改的属性将立即生效。
3、未知应用安装
未知应用安装权限的开关被除掉,取而代之的是未知来源应用的管理列表,需要在里面打开每个应用的未知来源的安装权限
//判断是否是AndroidO以及更高的版本
//Android8.0的变化是,未知应用安装权限的开关被除掉,取而代之的是未知来源应用的管理列表,需要在里面打开每个应用的未知来源的安装权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//是否有安装未知应用权限
val haveInstallPermission = mContext?.packageManager?.canRequestPackageInstalls()
if (haveInstallPermission == true) {
mContext?.startActivity(getInstallIntent(apkFile))
} else {
//未知来源应用的管理列表开启权限,安装应用需要打开未知来源权限,请去设置中开启权限
InstallPermissionDialog(mContext, R.style.Translucent_Dialog)
.apply {
setCancelable(false)
setCanceledOnTouchOutside(false)
setCancelListener {
Log.e("InstallException", "安装应用需要打开未知来源权限,请去设置中开启权限")
Toast.makeText(
mContext,
if (mContext == null) "安装应用需要打开未知来源权限,请去设置中开启权限" else mContext?.getString(
R.string.permission_dialog_content
),
Toast.LENGTH_LONG
).show()
}
setSubmitListener {
//跳转权限设置页面,需要使用onActivityResult中回调结果
InstallPermissionActivity.requestPermission(mContext!!,
object : InstallPermissionCallback {
override fun success(permissionActivity: Activity?) {
mContext?.startActivity(getInstallIntent(apkFile))
}
override fun failure(permissionActivity: Activity?) {
Log.e("InstallException", "安装应用需要打开未知来源权限,请去设置中开启权限")
Toast.makeText(
mContext,
if (mContext == null) "安装应用需要打开未知来源权限,请去设置中开启权限" else mContext?.getString(
R.string.permission_dialog_content
),
Toast.LENGTH_LONG
).show()
}
})
}
}
.show()
}
} else {
mContext?.startActivity(getInstallIntent(apkFile))
}
五:Android 9.0 (API 28)
Android 9.0是谷歌研发的移动端操作系统,开发代号为“Pie”(派),于2018年8月7日正式发布
1.利用wifi RTT 进行室内定位
Android 9 添加了对 IEEE 802.11mc Wi-Fi 协议(也称为 Wi-Fi Round-Trip-Time (RTT))的平台支持,从而让您的应用可以利用室内定位功能。
在运行 Android 9 且具有硬件支持的设备上,应用可以使用 RTT API 来测量与附近支持 RTT 的 Wi-Fi 接入点 (AP) 的距离。 设备必须已启用位置服务并开启 Wi-Fi 扫描(在 Settings > Location 下),同时您的应用必须具有 ACCESS_FINE_LOCATION 权限。
设备无需连接到接入点即可使用 RTT。 为了保护隐私,只有手机可以确定与接入点的距离;接入点无此信息。
如果您的设备测量与 3 个或更多接入点的距离,您可以使用一个多点定位算法来预估与这些测量值最相符的设备位置。 结果通常精准至 1 至 2 米。
通过这种精确性,您可以打造新的体验,例如楼内导航、基于精细位置的服务,如无歧义语音控制(例如,“打开这盏灯”),以及基于位置的信息(如 “此产品是否有特别优惠?”)。
2.显示屏缺口支持
Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout 类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,请使用 getDisplayCutout() 函数。
全新的窗口布局属性 layoutInDisplayCutoutMode 让您的应用可以为设备屏幕缺口周围的内容进行布局。 您可以将此属性设为下列值之一:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
可以按以下方法在任何运行 Android 9 的设备或模拟器上模拟屏幕缺口:
启用开发者选项。
在 Developer options 屏幕中,向下滚动至 Drawing 部分并选择 Simulate a display with a cutout。
选择屏幕缺口的大小。
注:我们建议您通过使用运行 Android 9 的设备或模拟器测试屏幕缺口周围的内容显示。
3.前台服务
如果应用以 Android 9 或更高版本为目标平台并使用前台服务,则必须请求 FOREGROUND_SERVICE 权限。这是普通权限,因此,系统会自动为请求权限的应用授予此权限。
如果以 Android 9 或更高版本为目标平台的应用尝试创建前台服务且未请求 FOREGROUND_SERVICE,则系统会抛出 SecurityException。
public class FloatPasswordWindowService extends Service {
@Override
public void onCreate() {
super.onCreate();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
LogUtils.d("FloatWindowService:onBind");
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand()");
// 参数一:唯一的通知标识;参数二:通知消息。
startForeground(110, notification);// 开始前台服务
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy()");
stopForeground(true);// 停止前台服务--参数:表示是否移除之前的通知
super.onDestroy();
}
}
9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限,否则系统会引发 SecurityException。
Intent intentService = new Intent(this, MyService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intentService);//开启前台服务
} else {
startService(intentService);
}
PS:表忘记AndroidManifest.xml中添加FOREGROUND_SERVICE权限
//9.0这个前台服务权限不要忘了
4.启动Activity
在9.0 中,不能直接非 Activity 环境中(比如Service,Application)启动 Activity,否则会崩溃报错。
这类问题一般会在点击推送消息跳转页面这类场景,解决方法就是 Intent 中添加标志FLAG_ACTIVITY_NEW_TASK
Intent intent = new Intent(this, TestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
5.Http请求
5.1:Okhttp网络请求时候报异常: java.net.UnknownServiceException: CLEARTEXT communication to ** not permitted by network security policy
原因是:当targetSdkVersion>=28时,OkHttp通过调用Android API中的isCleartextTrafficPermitted(host),在9.0系统上限制了Http明文请求。
方式一:把targetSdkVersion改成小于28版本
方式二:把请求接口改为https
方式三:通过配置Xml文件绕过Http限制
5.1.1.在res文件下创建一个xml文件,可以命名network_config:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
5.1.2.然后在AndroidMainfest.xml文件下的
<application
android:networkSecurityConfig="@xml/network_config">
</application>
5.2:Volley网络请求时候报异常:Cleartext HTTP traffic to * not permitted
方式一:把targetSdkVersion改成小于28版本
方式二:把请求接口改为https
方式三:5.2.1.在AndroidMainfest.xml文件下application节点下
<application
android:usesCleartextTraffic="true">
</application>
5.2.2.如果添加了上述代码程序还是无法运行,并出现闪退或报错“Didn’t find class “org.apache.http.ProtocolVersion” on path”的情况,在application节点
<application>
<uses-library
android:name="org.apache.http.legacy" android:required="false" />
</application>
6.Apache HTTP 客户端弃用
在 Android 6.0 时,就已经取消了对 Apache HTTP 客户端的支持。从 Android 9.0 开始,默认情况下该库已从 bootclasspath 中移除。但是耐不住有些SDK中还在使用,比如我见到的友盟QQ分享报错问题。
所以要想继续使用Apache HTTP,需要在应用的 AndroidManifest.xml 文件中添加:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyGitignore">
<uses-library android:name="org.apache.http.legacy" android:required="false"/>//使用这个
<application/>
7.多进程使用WebView时不支持同时从多个进程使用具有相同数据目录的WebView
解决方法:
多进程时在Applicationd类的onCreate或者onBaseContextAttached方法中加入
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
initWebViewDataDirectory(this);
}
/**
* 得到进程名称
* @param context
* @return
*/
public static String getProcessName(Context context) {
try {
if (context == null)
return null;
ActivityManager manager = (ActivityManager)
context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo :
manager.getRunningAppProcesses()) {
if (processInfo.pid == android.os.Process.myPid()) {
return processInfo.processName;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 为webView设置目录后缀
* @param context
*/
@RequiresApi(api = Build.VERSION_CODES.P)
public static void initWebViewDataDirectory(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
String processName = getProcessName(context);
if (!context.getPackageName().equals(processName)) {//判断是否是默认进程名称
WebView.setDataDirectorySuffix(processName);
}
}
六:Android 10 (API 29)
Android 10包含多项功能升级,包括手势导航、通知栏管理、全局黑暗模式等等,通知管理新增了“优先”、“无声”和“自适应通知”三种功能,新增深色主题的背景
1.用户存储权限的变更
Android Q 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”(例如 /sdcard)。任何其他应用都无法直接访问您应用的沙盒文件。由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。此变更可让您更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。在application中添加android:requestLegacyExternalStorage="true"可以禁用分区存储
谷歌官方推荐应用在沙盒内存储文件的地址为Context.getExternalFilesDir()下的文件夹。
比如要存储一张私有图片,则应放在Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)中;
比如要存储一张可分享其它应用的图片,则应放在Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)中。
getExternalFilesDir和getFilesDir文件路径:/storage/emulated/0/Android/data/包名/files
getExternalCacheDir和getCacheDir文件路径:/storage/emulated/0/Android/data/包名/cache
加External和不加的比较:
相同点:
1. 都可以做app缓存目录。
2. app卸载后,两个目录下的数据都会被清空。
不同点:
1、目录的路径不同。前者的目录存在外部SD卡上的。后者的目录存在app的内部存储上。
2、前者的路径在手机里可以直接看到。后者的路径需要root以后,用Root Explorer 文件管理器才能看到。
2.新增后台位置信息权限(非运行时权限)
如果应用中的某项功能会不断与其他用户分享位置信息或使用 Geofencing API,则该应用需要后台位置信息访问权限。以下是此类情况的几个示例:
- 在家庭位置信息分享应用中,某项功能可让用户与家庭成员持续分享位置信息。
- 在 IoT 应用中,某项功能可让用户配置自己的家居设备,使其在用户离家时关机并在用户回家时重新开机。
除了前台位置信息部分所述的情况之外,如果应用在任何其他情况下访问设备的当前位置信息,系统就会认为应用需要使用后台位置信息。后台位置信息精确度与前台位置信息精确度相同,具体取决于应用声明的位置信息权限。
在 Android 10(API 级别 29)及更高版本中,您必须在应用的清单中声明 ACCESS_BACKGROUND_LOCATION 权限,以便请求在运行时于后台访问位置信息。在较低版本的 Android 系统中,当应用获得前台位置信息访问权限时,也会自动获得后台位置信息访问权限。
<manifest ... >
<!-- Required only when requesting background location access on
Android 10 (API level 29) and higher. -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>
注意:Google Play 商店设置了有关设备位置信息的位置信息政策,限制应用仅在实现核心功能所必需的情形下且在满足相关政策要求后才能请求后台位置信息访问权限。
3.设备唯一标识符的变更
从 Android Q 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE签名权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。
如果您的应用没有该权限,但您仍尝试查询标识符的相关信息,会返回空值或报错。
设备唯一标识符需要特别注意,原来的READ_PHONE_STATE权限已经不能获得IMEI和序列。
如果想在Q设备上通过使用以下代码获取设备的ID
((TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId()
则执行以上代码会返回空值(targetSDK<=P)或者报错(targetSDK==Q)。且官方所说的READ_PRIVILEGED_PHONE_STATE权限只提供给系统app,所以这个方法行不通了。
谷歌官方给予了设备唯一ID最佳做法,但是此方法给出的ID可变,可以按照具体需求具体解决。本文给出一个不变和基本不重复的UUID方法:
public static String getUUID() {
String serial = null;
String m_szDevIDShort = "35" +
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
Build.USER.length() % 10; //13 位
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
serial = android.os.Build.getSerial();
} else {
serial = Build.SERIAL;
}
//API>=9 使用serial号
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
//serial需要一个初始化
serial = "serial"; // 随便一个初始化
}
//使用硬件信息拼凑出来的15位号码
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
4.新增媒体位置信息权限(非运行时权限)
如果您的应用以 Android 10(API 级别 29)或更高版本为目标平台,为了使您的应用从照片中检索未编辑的 Exif 元数据,您需要在应用的清单中声明 ACCESS_MEDIA_LOCATION 权限,然后在运行时请求此权限。
注意:由于您在运行时请求 ACCESS_MEDIA_LOCATION 权限,因此无法保证应用可以访问照片中未编辑的 Exif 元数据。应用需要用户明确同意才能访问此信息。
七:Android 11 (API 30)
Android 11正式版系统在2020年9月9日正式发布。系统主要增强了聊天气泡,安全性和隐私性的保护,电源菜单,可以更好的支持瀑布屏,折叠屏,双屏和 Vulkan 扩展程序等。
新特性
1.短信更新改进
首先是聊天泡泡。与Facebook多年来在Android上提供的Messenger应用程序类似,Android 11优化了短信功能,提供更加友好的交互。同时,为了确保用户能尽快收到对方的消息,Android 11在通知阴影(Notification Shade)中引入了一个专门的对话部分,它将提供对用户正在进行的任何对话的即时访问。这一更新将有助于短信消息从其他通知中脱颖而出。
2.电话号码相关权限
Android 11 更改了您的应用在读取电话号码时使用的与电话相关的权限。
其实就是两个API:
TelecomManager 类中的 getLine1Number() 方法
TelecomManager 类中的 getMsisdn() 方法
也就是当用到这两个API的时候,原来的READ_PHONE_STATE权限不管用了,需要READ_PHONE_NUMBERS权限才行。
3.现在需要 APK 签名方案 v2
对于以 Android 11(API 级别 30)为目标平台,且目前仅使用 APK 签名方案 v1 签名的应用,现在还必须使用 APK 签名方案 v2 或更高版本进行签名。用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用。
如果你的targetSdkVersion修改到30,那么你就必须要加上v2签名才行。否则无法安装和更新。
八:Android 12 (API 31-32)
Android 12 重新发现了代号为 “Columbus”的功能,并且优化了触发问题,新的手势需要更加用力敲击背面。新的双击背面手势可以截取屏幕截图、召唤谷歌 Assistant、打开通知栏、控制媒体播放或打开最近的应用程序列表。
1.AVIF图像支持
为了为您提供更高的图像质量和更有效的压缩,Android 12引入了对AV1图像文件格式(AVIF)的平台支持。AVIF是用于使用AV1编码的图像和图像序列的容器格式。与其他现代图像格式一样,AVIF利用了视频压缩中的帧内编码内容。与JPEG等较旧的图像格式相比,这可以显着提高相同文件大小的图像质量。
AVIF(18.2kB)JPEG(20.7kB)
2.前台服务优化
前台服务是应用程序管理某些类型的面向用户任务的重要方式,但是如果过度使用,它们可能会影响性能,甚至导致应用程序中断。为了确保为用户带来更好的体验,对于以新平台为目标的应用,我们将从后台阻止前台服务启动。为了更轻松地从此模式过渡,我们在JobScheduler中引入了一个新的加急作业,该作业获得了较高的进程优先级,网络访问权限,并且可以在不考虑节电或节电的情况下立即运行。为了实现向后兼容,我们还在最新版本的Jetpack WorkManager库中内置了加急作业。。另外,为了减少用户的注意力,我们现在将某些前台服务通知的显示延迟最多10秒钟。这使短暂的任务有机会在显示通知之前完成
3.android:exported
四大组件如果使用了 intent-filter,但是没显性质配置 exported App 将会无法安装,甚至编译不通过;
启动的 Activity 需要设置 exported 为 true;
com.android.tools.build:gradle:4.0.0 以及其下版本适配方案:
/**
* 修改 Android 12 因为 exported 的构建问题
*/
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
output.processResources.doFirst { pm ->
String manifestPath = output.processResources.manifestFile
def manifestFile = new File(manifestPath)
def xml = new XmlParser(false, true).parse(manifestFile)
def exportedTag = "android:exported"
///指定 space
def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
def nodes = xml.application[0].'*'.findAll {
//挑选要修改的节点,没有指定的 exported 的才需要增加
(it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(androidSpace.exported) == null
}
///添加 exported,默认 false
nodes.each {
def isMain = false
it.each {
if (it.name() == "intent-filter") {
it.each {
if (it.name() == "action") {
if (it.attributes().get(androidSpace.name) == "android.intent.action.MAIN") {
isMain = true
println("......................MAIN FOUND......................")
}
}
}
}
}
it.attributes().put(exportedTag, "${isMain}")
}
PrintWriter pw = new PrintWriter(manifestFile)
pw.write(groovy.xml.XmlUtil.serialize(xml))
pw.close()
}
}
}
com.android.tools.build:gradle:4.0.0 以上版本适配方案:
/**
* 修改 Android 12 因为 exported 的构建问题
*/
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.multiApkManifestOutputDirectory
File outputDirectory
if (outputDir instanceof File) {
outputDirectory = outputDir
} else {
outputDirectory = outputDir.get().asFile
}
File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
println("----------- ${manifestOutFile} ----------- ")
if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
def manifestFile = manifestOutFile
///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
def xml = new XmlParser(false, false).parse(manifestFile)
def exportedTag = "android:exported"
def nameTag = "android:name"
///指定 space
//def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
def nodes = xml.application[0].'*'.findAll {
//挑选要修改的节点,没有指定的 exported 的才需要增加
//如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
(it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
}
///添加 exported,默认 false
nodes.each {
def isMain = false
it.each {
if (it.name() == "intent-filter") {
it.each {
if (it.name() == "action") {
//如果 nameTag 拿不到可以尝试 it.attribute(androidSpace.name)
if (it.attributes().get(nameTag) == "android.intent.action.MAIN") {
isMain = true
println("......................MAIN FOUND......................")
}
}
}
}
}
it.attributes().put(exportedTag, "${isMain}")
}
PrintWriter pw = new PrintWriter(manifestFile)
pw.write(groovy.xml.XmlUtil.serialize(xml))
pw.close()
}
}
}
}
com.android.tools.build:gradle:4.2.0 以上版本适配方案:
从 gradle:4.2.0 & gradle-6.7.1-all.zip 开始,TargetSDK 31 下脚本会有异常,因为在 processDebugMainManifest (带有 Main) 的阶段,会直接扫描依赖库的 AndroidManifest.xml 然后抛出直接报错,从而进不去 processDebugManifest 任务阶段就编译停止,所以实际上脚本并没有成功运行。
该脚本的作用是在运行时自动帮您打印出现问题的 aar 包依赖路径和组建名称:
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
//println("=============== ${variant.getBuildType().name.toUpperCase()} ===============")
//println("=============== ${variant.getFlavorName()} ===============")
def vn
if (variant.getFlavorName() != null && variant.getFlavorName() != "") {
vn = variant.name.substring(0, 1).toUpperCase() + variant.name.substring(1)
} else {
if (variant.getBuildType().name == "release") {
vn = variant.productFlavors[0].name.substring(0, 1).toUpperCase() + variant.productFlavors[0].name.substring(1) + "Release"
} else {
vn = variant.productFlavors[0].name.substring(0, 1).toUpperCase() + variant.productFlavors[0].name.substring(1) + "Debug"
}
}
def taskName = "process${vn}MainManifest";
try {
println("=============== taskName ${taskName} ===============")
project.getTasks().getByName(taskName)
} catch (Exception e) {
return
}
///您的自定义名字
project.getTasks().getByName(taskName).doFirst {
//def method = it.getClass().getMethods()
it.getManifests().getFiles().each {
if (it.exists() && it.canRead()) {
def manifestFile = it
def exportedTag = "android:exported"
def nameTag = "android:name"
///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
def xml = new XmlParser(false, false).parse(manifestFile)
if (xml.application != null && xml.application.size() > 0) {
def nodes = xml.application[0].'*'.findAll {
//挑选要修改的节点,没有指定的 exported 的才需要增加
//如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
(it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
}
if (nodes.application != null && nodes.application.size() > 0) {
nodes.each {
def t = it
it.each {
if (it.name() == "intent-filter") {
println("$manifestFile \n .....................${t.attributes().get(nameTag)}......................")
}
}
}
}
}
}
}
}
}
}
4.位置权限变更
使用针对Android 12的应用程序时,用户可以要求该应用程序只能访问大概的位置信息。为了更好地尊重用户隐私,建议您仅请求ACCESS_COARSE_LOCATION。如果您的应用面向Android 12并请求ACCESS_FINE_LOCATION运行时权限,则您还必须请求ACCESS_COARSE_LOCATION权限。您必须在单个运行时请求中同时包含两个权限。
5.新增附近设备权限
如果您的应用以 Android 12(API 级别 31)或更高版本为目标平台,请在应用的清单文件中声明以下权限:
- 如果您的应用程序寻找蓝牙设备,例如 BLE 外围设备,请声明 BLUETOOTH_SCAN权限。
- 如果您的应用程序使当前设备可被其他蓝牙设备发现,请声明该 BLUETOOTH_ADVERTISE 权限。
- 如果您的应用程序与已配对的蓝牙设备通信,请声明 BLUETOOTH_CONNECT权限。
- 对于遗留的蓝牙相关权限声明,设置android:maxSdkVersion为30. 此应用兼容性步骤有助于系统仅向您的应用授予安装在运行 Android 12 或更高版本的设备上时所需的蓝牙权限。
- 如果您的应用使用蓝牙扫描结果来获取物理位置,请声明 ACCESS_FINE_LOCATION权限。否则,您可以强烈断言您的应用不会获取物理位置。
BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT 和 BLUETOOTH_SCAN权限是运行时权限。因此,您必须在您的应用程序中明确请求用户批准,然后才能查找蓝牙设备、使设备可被其他设备发现或与已配对的蓝牙设备通信。当您的应用请求至少其中一项权限时,系统会提示用户允许您的应用访问 附近的设备。
以下代码片段演示了如果您的应用以 Android 12 或更高版本为目标,如何在您的应用中声明与蓝牙相关的权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 在旧设备上请求传统蓝牙权限。-->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- 仅当您的应用寻找蓝牙设备时才需要。如果您的应用不使用蓝牙扫描结果来获取物理位置信息,您可以强烈断言您的应用不获取物理位置。-->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 仅当您的应用使设备可被蓝牙设备发现时才需要。-->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 仅当您的应用程序与已配对的蓝牙设备通信时才需要。-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 仅当您的应用使用蓝牙扫描结果获取物理位置时才需要。-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
九:Android 13 (API 33)
1.细化的媒体权限
从Android 13开始,以Android13(API 33+)为目标平台的应用,系统新增运行时权限READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO 替代原有的READ_EXTERNAL_STORAGE权限。
如果用户之前向您的应用授予了 READ_EXTERNAL_STORAGE 权限,系统会自动向您的应用授予细化的媒体权限。否则,当应用请求上表中显示的任何权限时,系统会显示面向用户的对话框。在图 1 中,应用请求 READ_MEDIA_AUDIO 权限。
如果您同时请求 READ_MEDIA_IMAGES 权限和 READ_MEDIA_VIDEO 权限,系统只会显示一个系统权限对话框。
注意:如果您的应用只需要访问图片、照片和视频,请考虑使用照片选择器,而不是声明 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 权限。
2. 新增通知运行时权限
Android 13 中引入了新的运行时权限,用于从应用发送非豁免通知:POST_NOTIFICATIONS。此更改有助于用户专注于最重要的通知。
强烈建议您尽快以 Android 13 为目标平台,以获享此功能提供的额外控制和灵活性。如果您继续以 12L(API 级别 32)或更低版本为目标平台,您将失去在应用功能环境中请求权限的机会。
应用的影响总结如下:
2.1 使用新权限
如需向应用请求新的通知权限,请将应用更新为以 Android 13 为目标平台,并完成与请求其他运行时权限类似的流程,如以下几个部分所述。需要在应用的清单文件中声明的权限会显示在以下代码段中:
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
...
</application>
</manifest>
2.2 应用功能取决于用户在权限对话框中所做的选择
在此对话框中,用户可执行以下操作:
- 选择允许
- 选择不允许
- 滑开对话框,不按任何一个按钮
注意:如果用户点按不允许,即使仅点按一次,系统也不会再次提示用户,除非他们卸载并重新安装您的应用。
下面几个部分介绍了根据用户操作的不同,应用会有哪些不同的行为表现。
a. 用户选择“允许”如果用户选择允许选项,您的应用可以执行以下操作:
- 发送通知。可以使用所有通知渠道。
- 发送与前台服务相关的通知。这些通知会显示在抽屉式通知栏中。
b. 用户选择“不允许”如果用户选择不允许选项,您的应用将无法发送通知。除了几个特定角色之外,所有通知渠道都会被屏蔽。这类似于用户在系统设置中手动关闭应用的所有通知后发生的行为。
c. 用户滑开对话框
- 如果用户滑开对话框(即他们既没有选择允许,也没有选择不允许),会发生以下行为:
- 如果您的应用符合获得临时通知授权的条件,系统会保留临时授权。
- 如果您的应用没有临时授权,则将无法发送通知。
2.3 对新安装的应用的影响
如果用户在搭载 Android 13 的设备上安装您的应用,应用的通知默认处于关闭状态。在您请求新的权限且用户向您的应用授予该权限之前,您的应用都将无法发送通知。
权限对话框的显示时间取决于应用的目标 SDK 版本:
- 如果您的应用以 Android 13 或更高版本为目标平台,应用将可以完全自行控制权限对话框的显示时间。您可以借此机会向用户说明应用需要此权限的原因,进而鼓励他们授予该权限。
- 如果您的应用以 12L(API 级别 32)或更低版本为目标平台,系统会在您创建第一个通知渠道时显示权限对话框。这通常是在应用启动时。
2.4 对现有应用更新的影响
注意:请考虑下面这种情形,即应用安装在搭载 12L 或更低版本的旧设备上,用户在该设备上允许接收通知,但想淘汰该设备。用户现在使用的是搭载 Android 13 的新设备,并通过备份和恢复功能恢复了应用。
在这种情况下,系统会将您的应用视为“现有应用”,因此该应用会获得发送通知的临时权限。
为最大限度地减少与新通知权限相关的中断,系统会自动向系统升级到 Android 13 之前用户设备上已安装的所有符合条件的应用临时授予新通知权限。此临时授权的持续时间取决于应用的目标 SDK 版本:
- 如果应用以 Android 13 或更高版本为目标平台,则临时授权将持续到应用首次启动 activity 为止。
您的应用可以完全自行控制权限对话框的显示时间。您可以借此机会向用户说明应用需要此权限的原因,进而鼓励他们授予该权限。
- 如果您的应用以 12L 或更低版本为目标平台,则临时授权将一直有效,直到用户在通知权限运行时对话框中明确选择一个选项。也就是说,如果用户在未做出选择的情况下关闭了权限提示,系统会保留应用的临时授权。
2.5 获得临时授权的资格要求
您的应用要获得临时授权必须满足以下条件:应用必须已具有通知渠道,并且用户未在搭载 12L 或更低版本的设备上明确停用应用的通知。
如果用户在搭载 12L 或更低版本的设备上停用了应用的通知,当设备升级到 Android 13 或更高版本后,该停用会继续有效。
2.6 豁免
与媒体会话有关的通知不受此行为变更的影响。
2.7 最佳做法
本部分将介绍几种在应用中最有效地使用新通知权限的方式。
a. 更新应用的目标 SDK 版本
为了让应用更灵活地显示权限对话框,请将应用更新为以 Android 13 为目标平台。
b. 等待一段时间再显示通知权限提示
等到用户熟悉您的应用之后,再请求他们授予任何权限。
新用户可能想要探索您的应用,并切身体会每项通知请求可以带来的好处。您可以通过用户操作触发权限提示。下面列举了几个适合显示通知权限提示的时机:
- 用户点按“提醒铃铛”按钮时。
- 用户选择关注他人的社交媒体帐号时。
- 用户提交外卖订单时。
下图显示了请求通知权限的建议工作流程。或者,您也可以设置一个请求以在应用启动时显示,但仅在应用第 3 次或第 4 次启动后才显示。
c. 在上下文中请求权限
在应用内请求通知权限时,请在正确的上下文中请求,以便用户明确了解通知的用途以及应该选择接收通知的原因。例如,电子邮件应用可能包含为每封新邮件发送通知的选项,或仅为用户是唯一收件人的邮件发送通知的选项。
借此机会明确向用户表明您的意图,有助于鼓励用户向您的应用授予通知权限。
d. 检查您的应用能否发送通知
用户必须为您的应用启用通知,您的应用才能发送通知。要确认用户是否已启用通知,请调用areNotificationsEnabled()。
e. 以负责任的方式使用权限
获得发送通知的许可后,请负责任地使用该权限。用户可以查看您的应用每天发送的通知数量,并且可以随时撤消该权限。
3. 新增附近 Wi-Fi 设备运行时权限
Android 13 引入了NEARBY_WIFI_DEVICES运行时权限,该权限属于NEARBY_DEVICES权限组,适用于会管理设备与附近 Wi-Fi 接入点连接情况的应用。借助此权限,您可以更轻松地说明应用为何访问附近的 Wi-Fi 设备;在以前的 Android 版本中,这类应用需要声明ACCESS_FINE_LOCATION权限。
如果您的应用以 Android 13 为目标平台并调用多个不同的 Wi-Fi API,则必须从用户处获得这项新权限。
如果您的应用尝试在未获得适当权限的情况下调用 Wi-Fi API,则会发生SecurityException。
3.1 受影响的用例
这项新权限会影响几个不同的 Wi-Fi 用例,包括以下用例:
- 查找或连接到附近的设备,如打印机或媒体投射设备。通过该工作流,您的应用可以完成以下类型的任务: 通过带外方式(例如通过 BLE)接收 AP 信息。
- 使用仅限本地使用的热点,通过 Wi-Fi 感知和连接功能发现并连接到设备。
- 通过 Wi-Fi 直连发现和连接到设备。
- 发起与已知 SSID(例如汽车或智能家居设备)的连接。
- 开启仅限本地使用的热点。
- 连接到附近的 Wi-Fi 感知设备。
3.2 该权限属于“附近的设备”权限组
NEARBY_WIFI_DEVICES权限是附近的设备权限组的一部分。此权限组在 Android 12(API 级别 31)中添加,还包含与蓝牙和超宽带相关的权限。如果您的应用请求此权限组中的多项权限,用户会看到一个运行时对话框,其中会请求用户批准您的应用访问附近的设备。在系统设置中,用户必须以组的形式启用和停用附近的设备权限;例如,针对给定应用,用户无法既停用其 Wi-Fi 访问权限,但又保持启用其蓝牙使用权限。
3.3 坚定地声明您的应用不会推导物理位置
在以 Android 13 为目标平台时,请考虑您的应用是否会通过 Wi-Fi API 推导物理位置;如果不会,则应坚定声明此情况。如需做出此声明,请在应用的清单文件usesPermissionFlags属性设为neverForLocation,如以下代码段所示。此过程类似于您声明绝不会将蓝牙设备信息用于获取位置信息时的过程:
<manifest ...>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<application ...>
...
</application>
</manifest>
3.4 保持后向兼容性
由于NEARBY_WIFI_DEVICES权限仅适用于 Android 13 或更高版本,因此您应保留对ACCESS_FINE_LOCATION的所有声明,以便在您的应用中提供向后兼容性。不过,只要您坚定声明应用不会使用 Wi-Fi API 推导物理位置信息,就可以将此权限的最高 SDK 版本设为32,如下以下代码段所示:
<manifest ...>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32" />
<application ...>
...
</application>
</manifest>
3.5 某些 API 仍需要位置信息权限
有几个 Wi-Fi API 仍然需要ACCESS_FINE_LOCATION权限才能获取位置信息,就像它们在 12L 及更低版本上的一样。示例包括WifiManager类中的以下方法:
- getScanResults()
- startScan()
3.6 检查需要新权限的 API
如果您的应用以 Android 13 或更高版本为目标平台,您必须声明NEARBY_WIFI_DEVICES权限才能调用以下任何 Wi-Fi API:
- WifiManager startLocalOnlyHotspot()
- WifiAwareManager attach()
- WifiAwareSession publish()
- subscribe()
- WifiP2pManager addLocalService()
- connect()
- createGroup()
- discoverPeers()
- discoverServices()
- requestDeviceInfo()
- requestGroupInfo()
- requestPeers()
- WifiRttManager startRanging()
4. 新增身体传感器运行时权限
Android 13 中引入了“在使用时”访问身体传感器(例如心率、体温和血氧饱和度)的概念。此访问模式与Android 10(API 级别 29)系统为位置信息引入的模式非常相似。
如果您的应用以 Android 13 为目标平台,并且在后台运行时需要访问身体传感器信息,那么除了现有的BODY_SENSORS权限外,您还必须声明新的BODY_SENSORS_BACKGROUND权限。
注意:这是受到“硬性限制”的权限,除非设备的安装程序针对您的应用将该权限列入了许可名单,否则您的应用将无法获得此权限。如需了解详情,请参阅有关受限权限的指南。
5. intent 过滤器会屏蔽不匹配的 intent
当您的应用向以 Android 13 或更高版本为目标平台的其他应用的导出组件发送 intent 时,仅当该 intent 与接收应用中的元素匹配时,系统才会传送该 intent。换言之,系统会屏蔽所有不匹配的 intent,但以下情况除外:
- 发送给其他应用的未声明任何 intent 过滤器的组件的 intent。
- 发送给您应用中的其他组件的 intent。
- 由系统发送的 intent。
- 由具有根级特权的用户发送的 intent。
6. 更安全地注册广播接收器
为了帮助提高运行时接收器的安全性,Android 13 允许您指定您应用中的特定广播接收器是否应被导出以及是否对设备上的其他应用可见。如果导出广播接收器,其他应用将可以向您的应用发送不受保护的广播。此导出配置在以 Android 13 或更高版本为目标平台的应用中可用,有助于防止一个主要的应用漏洞来源。
在以前的 Android 版本中,设备上的任何应用都可以向动态注册的接收器发送不受保护的广播,除非该接收器受签名权限的保护。
要实现此安全增强措施,请执行以下操作:
a. 启用DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED兼容性框架更改。
b. 在应用的每个广播接收器中,明确指明其他应用是否可以向其发送广播,如以下代码段所示:
// This broadcast receiver should be able to receive broadcasts from other apps.
// This option causes the same behavior as setting the broadcast receiver's
// "exported" attribute to true in your app's manifest.
context.registerReceiver(sharedBroadcastReceiver, intentFilter,
RECEIVER_EXPORTED);
// For app safety reasons, this private broadcast receiver should **NOT**
// be able to receive broadcasts from other apps.
context.registerReceiver(privateBroadcastReceiver, intentFilter,
RECEIVER_NOT_EXPORTED);
注意:如果启用了DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED兼容性框架更改,则必须为每个广播接收器指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED。否则,当您尝试注册广播接收器时,系统会抛出SecurityException。
为了帮助检测这种情况,Android Studio 将会采用一个 lint 规则,而即将发布的ContextCompat将会检查接收器配置是否正确。
6.剪切板内容隐藏
Android 13(API 33)开始,用户可以选择使用API PersistableBundle#(ClipDescription.EXTRA_IS_SENSITIVE, true)隐藏要复制到剪切板的用户账户、密码登敏感信息。
相关API使用举例如下:
private void addData2Clipboard() {
ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = ClipData.newPlainText("111111", "我是密码");
ClipDescription description = clipData.getDescription();
// 隐私内容:剪切板加密
PersistableBundle persistableBundle = new PersistableBundle();
if (Build.VERSION.SDK_INT >= 33) {
persistableBundle.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
} else {
persistableBundle.putBoolean("android.content.extra.IS_SENSITIVE", true);
}
description.setExtras(persistableBundle);
// 剪切板添加加密内容
clipboardManager.setPrimaryClip(clipData);
}