面向 Android 12 的行为变更

与早期版本一样,Android 12包含可能会影响您的应用程序的行为更改。以下行为更改仅适用于以Android 12或更高版本为目标的应用。如果您的应用程序针对Android 12,则应在适用的情况下修改您的应用程序以正确支持这些行为。


前言

2021年2月18号

Android 12 的第一个开发者预览版(DP1 - Developer Preview 1)正式发布

之后,在3月3日发布了内部版本为SPP1.210122.022的DP1.1

能支持该系统的最低Pixel设备也就是Pixel 3

这个周末 终于能腾出一点时间 写点笔记了 : )

来吧,这次一起来简单的看看 目标版本为12的相关行为变化

文档基于: Android 12 DP1 / DP1.1
运行环境: Windows 10 、Android Studio 4.1.2 、 Android Emulator API S

安全

组件暴露安全问题

如果您的应用以Android 12为目标并且包含使用intent filtersactivitiesservices以及broadcast receivers ,则必须为这些应用组件显式声明android:exported属性。

警告:一个activity, service, 或者 broadcast receiver 使用了 intent filters , 但是并没有显式声明android:exported属性值,您的app将无法被安装到运行android 12的设备上

当使用Android Studio时,你试图去安装该app, Logcat将会展示如下的错误消息:

1
2
3
4
5
Installation did not succeed.
The application could not be installed: INSTALL_FAILED_VERIFICATION_FAILURE
List of apks:
[0] '.../build/outputs/apk/debug/app-debug.apk'
Installation failed due to: 'null'

如果您的应用在需要时未声明android:exported的值,则Logcat提供以下错误消息:

1
2
Targeting S+ (version 10000 and above) requires that an explicit value for \
android:exported be defined when intent filters are present

以下代码段显示了一个包含意图过滤器且已针对Android 12正确配置的服务示例:

1
2
3
4
5
6
<service android:name="com.example.app.backgroundService"
android:exported="false">
<intent-filter>
<action android:name="com.example.app.START_BACKGROUND" />
</intent-filter>
</service>

示例

创建一个 targetSdkVersion “S” 的新项目

  • CASE 1 不添加 android:exported 属性

项目构建完成,默认的情况下activity下是没有 android:exported 属性

1
2
3
4
5
6
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

运行项目,Run 窗口出现错误信息

20210313_095133_18

Logcat窗口 下显示错误日志

20210313_095516_93

设备上没有应用安装成功 ~~ Hmm….

  • CASE 2 添加上 android:exported 属性

添加 android:exported=”false” 到 MainActivity 节点内

1
2
3
4
5
6
7
8
<activity
android:name=".MainActivity"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

运行项目,,Run 窗口出现如下成功消息

20210313_100606_28

手机上的APP没有启动起来,但是安装成功了

20210313_100748_84

然后手动去点击APP,启动它, 提示 应用未安装

20210313_101307_47

切换下LogcatError 看看有没有什么发现? 抛出了 安全性异常 ….

20210313_101626_54

?????????????? TM 居然运行一个例子跑不起来? WTF !!!

20210313_101947_36

好吧 ~ 你赢了! 那可能我的打开方式不对吧,重新再试一次

false不行,那我试试 true

1
2
3
4
5
6
7
8
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

走你 Run

20210313_102908_30

20210313_103208_10

20210313_103252_91

Hmmm … 第一个Android S 的例子,可算把你成功运行起来了。

那么,问题来了 ?

Q : 为什么 MainActivity 必须要设置 android:exported=”true” ?
A : 略.(留给大家回答了)

程序运行起来了,我们继续后面的内容

Pending intents 必须声明可变性

如果您的应用程序针对Android 12,则必须指定PendingIntent应用程序创建的每个对象的可变性。此附加要求可提高应用程序的安全性。

要声明给定PendingIntent对象是可变的或不可变的 ,请分别使用 PendingIntent.FLAG_MUTABLE 可变标志PendingIntent.FLAG_IMMUTABLE 不可变标志。如果您的应用尝试在PendingIntent未设置任何可变性标志的情况下创建对象,则系统将抛出 IllegalArgumentException,并且Logcat中将显示以下消息:

1
2
3
4
5
6
PACKAGE_NAME: Targeting S+ (version 10000 and above) requires that one of \
FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.

Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if \
some functionality depends on the PendingIntent being mutable, e.g. if \
it needs to be used with inline replies or bubbles.

尽可能创建不可变的 Pending intents
在大多数情况下,您的应用应创建不可变的PendingIntent对象,如以下代码片段所示。如果PendingIntent对象是不可变的,则应用程序无法修改意图以调整调用意图的结果。

1
2
3
val pendingIntent = PendingIntent.getActivity(applicationContext,
REQUEST_CODE, intent,
/* flags */ PendingIntent.FLAG_IMMUTABLE)

但是,某些应用程序需要创建可变PendingIntent对象,而不是:

  • 一个通知中直接回复动作需要改变的剪辑数据PendingIntent是与答复相关联的对象。通常,您可以通过将FILL_IN_CLIP_DATA标记作为fillIn()方法传递给方法来 请求此更改。
  • 如果您的应用 使用a将对话放入气泡PendingIntent,则意图应该是可变的,以便系统可以应用正确的标志,例如 FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT

如果您的应用创建了可变PendingIntent对象,则强烈建议您使用明确的意图 并填写 ComponentName。这样,每当另一个应用程序调用PendingIntent并将控制权传递回您的应用程序时,该应用程序中的同一组件始终会启动。

测试可变性Pending intents 更改

要确定您的应用是否缺少可变性声明,请在Android Studio中查找以下lint警告

1
Warning: Missing PendingIntent mutability flag [UnspecifiedImmutableFlag]

在开发者预览期间,您可以通过关闭 PENDING_INTENT_EXPLICIT_MUTABILITY_REQUIRED 应用程序兼容性标志来禁用此系统行为以进行测试。

示例

在 MainActivity 中创建一个意图通知

1
2
3
4
5
6
7
8
9
//创建一个意图
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.google.cn/"))
val pIntent = PendingIntent.getActivity(this, 1, intent, 0)
//setContentIntent 将意图设置到通知上
builder.setContentIntent(pIntent)
//构建通知
val notification = builder.build()
//将构建好的通知添加到通知管理器中,执行通知
mNotificationManager.notify(0, notification)

因为缺少可变性声明,没有声明,抛出异常

20210313_115401_99

FLAG_IMMUTABLE (不可变性) 声明 @RequiresApi(23)

1
val pIntent = PendingIntent.getActivity(this, 1, intent, /*flags*/ PendingIntent.FLAG_IMMUTABLE)

可变性声明: 指示创建的PendingIntent应该是不可变的标志。这意味着传递给send方法以填充此intent的未填充属性的其他intent参数将被忽略。FLAG_IMMUTABLE仅限制了​​更改send()由的调用者 发送的意图的语义的能力send()。PendingIntent的创建者始终可以通过来更新PendingIntent本身FLAG_UPDATE_CURRENT。

FLAG_MUTABLE (可变性) 声明 @RequiresApi(31)

1
val pIntent = PendingIntent.getActivity(this, 1, intent, /*flags*/ PendingIntent.FLAG_MUTABLE)

可变性声明: 指示创建的PendingIntent应该是可变的标志。此标志不能与组合FLAG_IMMUTABLE。
直到 Build.VERSION_CODES.R,除非FLAG_IMMUTABLE已设置,否则默认情况下都假定PendingIntents是可变的。从开始Build.VERSION_CODES.S,将需要使用(@link #FLAG_IMMUTABLE}或FLAG_MUTABLE。显式指定创建时PendingIntents的可变性。强烈建议FLAG_IMMUTABLE在创建PendingIntent时使用。FLAG_MUTABLE仅当某些功能依赖于修改基础意图时才应使用。例如需要与内联回复或气泡一起使用的任何PendingIntent。

性能

前台服务通知不允许在后台开启

除了一些特殊情况,以Android 12为目标的应用在后台运行时将无法再启动前台服务。如果应用程序在后台运行时尝试启动前台服务,则会发生异常(少数特殊情况除外),考虑在您的应用程序在后台运行时使用WorkManager调度和开始工作。

示例

创建一个前台服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.xw.androidstest

import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log

class MyService : Service() {

private val tag = "MyService"

override fun onBind(intent: Intent): IBinder? {
throw UnsupportedOperationException("Not yet implemented");
}

override fun onCreate() {
super.onCreate()
Log.d(tag, "onCreate()");
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(tag, "onStartCommand()")
foregroundNotify()
return super.onStartCommand(intent, flags, startId)
}

private fun foregroundNotify() {
Log.d(tag, "foregroundNotify() called")
val pendingIntent: PendingIntent =
Intent(this, MainActivity2::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
val notification: Notification = Notification.Builder(this, "通知渠道ID")
.setContentTitle("前台服务标题")
.setContentText("前台服务内容")
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.build()

// Notification ID cannot be 0.
startForeground(10, notification)
}

override fun onDestroy() {
Log.d(tag, "onDestroy()");
stopForeground(true);// 停止前台服务--参数:表示是否移除之前的通知
super.onDestroy()
}

}

添加权限

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

在MainActivity 中调用

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Timer().schedule(object : TimerTask() {
override fun run() {
Log.d(tag, "run() called")
startService(Intent(this@MainActivity, MyService::class.java))
}
}, 5000)
}

程序运行起来了,然后按Home键回到launcher, 将app切到后台 等待效果。

20210313_163622_86

20210313_163720_55

20210313_163552_95

无法从服务或广播接收者创建通知跳板

当用户与通知交互时,某些应用程序会通过启动应用程序组件来响应通知点击,该组件最终会启动用户最终看到并与之交互的Activity。此应用程序组件称为通知跳板

为了提高应用程序性能和UX,面向Android 12的应用程序无法从用作通知蹦床的服务或 广播接收器启动活动 。换句话说,用户点击通知或通知内的操作按钮后,应用程序无法在 Service 或 BroadcastReceiver 内调用startActivity()

当您的应用尝试从充当通知蹦床的服务或广播接收器启动活动时,系统将阻止该活动启动,并且Logcat中将显示以下消息 :

1
2
Indirect notification activity start (trampoline) from PACKAGE_NAME, \
this should be avoided for performance reasons.

更新您的应用

如果您的应用从充当通跳板的服务或广播接收器启动活动,请完成以下迁移步骤:

  1. 创建PendingIntent与以下活动之一关联的对象:

    • 用户点击通知后看到的Activity(首选)。
    • 跳板Activity或启动用户点击通知后看到的Activity的Activity
  2. 使用PendingIntent在上一步中创建的对象作为构建通知的一部分。

切换行为
在开发人员预览版中测试应用程序时,可以使用NOTIFICATION_TRAMPOLINE_BLOCK应用程序兼容性标志启用和禁用此限制。

示例

新建了一个服务、一个广播、一个MainActivity2 , 分别在服务和广播里面去启动 MainActivity2

MyService.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyService : Service() {

override fun onBind(intent: Intent): IBinder? {
return null
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.let {
intent.setClass(this,MainActivity2::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
return super.onStartCommand(intent, flags, startId)

}
}

MyReceiver.kt

1
2
3
4
5
6
7
8
9
10
class MyReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
intent.let {
intent.setClass(context,MainActivity2::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
}

MainActivity2.kt

1
2
3
4
5
6
class MainActivity2 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
}
}

最后分别在MainActivity里进行两种情况处理

1
2
3
4
5
6
7
8
9
//创建一个服务意图 点击通知栏消息启动服务
val intent = Intent()
intent.setClass(this, MyService::class.java)
val pIntent = PendingIntent.getService(this, 1, intent, /*flags*/ PendingIntent.FLAG_IMMUTABLE)

//创建一个广播意图 点击通知栏消息启动广播
val intent = Intent()
intent.setClass(this, MyReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(this, 1, intent, /*flags*/ PendingIntent.FLAG_IMMUTABLE)

通过shell切换行为:

1
2
adb shell am compat enable 167676448 com.xw.androidstest
adb shell am compat disable 167676448 com.xw.androidstest

20210313_155949_11

1
2
3
4
2021-03-13 23:58:24.356 533-1591/system_process I/ActivityTaskManager: START u0 {flg=0x10000000 cmp=com.xw.androidstest/.MainActivity2} from uid 10177
2021-03-13 23:58:24.358 533-1591/system_process E/NotificationService: Indirect notification activity start (trampoline) from com.xw.androidstest blocked
2021-03-13 23:58:24.359 533-1591/system_process W/ActivityTaskManager: Background activity start [callingPackage: com.xw.androidstest; callingUid: 10177; appSwitchAllowed: true; balAppSwitchEnabled: true; isCallingUidForeground: false; callingUidHasAnyVisibleWindow: false; callingUidProcState: SERVICE; isCallingUidPersistentSystemProcess: false; realCallingUid: 10177; isRealCallingUidForeground: false; realCallingUidHasAnyVisibleWindow: false; realCallingUidProcState: SERVICE; isRealCallingUidPersistentSystemProcess: false; originatingPendingIntent: null; allowBackgroundActivityStart: false; intent: Intent { flg=0x10000000 cmp=com.xw.androidstest/.MainActivity2 }; callerApp: ProcessRecord{30029ba 25657:com.xw.androidstest/u0a177}]
2021-03-13 23:58:24.360 533-1591/system_process E/ActivityTaskManager: Abort background activity starts from 10177

自定义通知变更

  • 原状:以前,自定义通知可以使用整个通知区域,并提供自己的布局和样式。这导致可能会混淆用户或导致不同设备上的布局兼容性问题。
  • 现状:针对 targetSdkVersion Android 12,自定义通知不再使用完整的通知区域,android12 提供了系统应用标准模板;
    模板可确保自定义通知与所有状态下的其他通知具有相同的修饰(人人平等),例如通知的图标和扩展提示(处于折叠状态)以及通知的图标、应用程序名称和折叠提示(处于扩展状态)。
    Android 12 通过这种方式使得所有通知在视觉上保持一致,为用户提供了一个可发现的、熟悉的通知扩展。

标准模板中的自定义通知:

20210313_132122_31

折叠与非折叠状态下的样式

20210313_132237_84

官方建议使用了构建自定义通知的相关接口的应用,都要重新适配,相关接口有:

  • setCustomContentView(RemoteViews)
  • setCustomBigContentView(RemoteViews)
  • setCustomHeadsUpContentView(RemoteViews)

如果您的应用使用的是完全自定义的通知,建议您尽快使用新模板进行测试并进行必要的调整:

  1. 启用自定义通知更改:

    • 改变你的应用程序的targetSdkVersion,以S开启新的行为。
    • 重新编译。
    • 在运行Android 12的设备或模拟器上安装您的应用。
  2. 测试所有使用自定义视图的通知,确保它们在阴影中看起来像您期望的那样。

  3. 请注意自定义视图的度量。通常,自定义通知的高度要小于以前。在折叠状态下,自定义内容的最大高度已从106dp降低到48dp。同样,水平空间也更少。

  4. 为了确保“Heads Up”状态看起来像您期望的那样,请不要忘记将通知通道的重要性提高到“HIGH”(屏幕弹出)。

示例

创建一个自定义的通知

remote_view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_margin="10dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_gravity="center"
android:layout_width="60dp"
android:src="@drawable/icon"
android:layout_height="60dp"/>
<LinearLayout
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:layout_marginStart="10dp"
android:id="@+id/music_name"
android:textSize="20dp"
android:text="可可托海的牧羊人"
android:maxLines="1"
android:ellipsize="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginStart="10dp"
android:layout_marginTop="6dp"
android:id="@+id/singer_name"
android:textSize="16dp"
android:text="王琪"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

</LinearLayout>
</LinearLayout>

<LinearLayout
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<ImageButton
android:id="@+id/btn_prev"
android:background="@drawable/ic_baseline_skip_previous_24"
android:layout_width="40dp"
android:layout_height="40dp"/>
<ImageButton
android:layout_marginStart="10dp"
android:id="@+id/btn_play"
android:src="@drawable/ic_baseline_play_arrow_24"
android:background="#00ffffff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageButton
android:layout_marginStart="10dp"
android:id="@+id/btn_next"
android:background="@drawable/ic_baseline_skip_next_24"
android:layout_width="40dp"
android:layout_height="40dp"/>
</LinearLayout>
</LinearLayout>

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private fun notification() {
val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val builder: NotificationCompat.Builder?
val channel = NotificationChannel(
"通知渠道ID",
"通知渠道名称", NotificationManager.IMPORTANCE_DEFAULT
)
channel.description = "通知渠道描述"
mNotificationManager.createNotificationChannel(channel)
builder = NotificationCompat.Builder(this, "通知渠道ID")

builder.setSmallIcon(R.drawable.ic_baseline_music_note_24)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))

val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.google.cn/"))
val pIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_IMMUTABLE)
builder.setContentIntent(pIntent)

val remoteViews = RemoteViews(this.packageName, R.layout.remote_view)

builder.setCustomContentView(remoteViews)
builder.setCustomBigContentView(remoteViews)
builder.setCustomHeadsUpContentView(remoteViews)


builder.setDefaults(NotificationCompat.DEFAULT_ALL)
val notification = builder.build()
mNotificationManager.notify(0, notification)

}

运行一下效果呢:

折叠状态

20210313_143947_68

展开状态

20210313_144023_82

修改一下折叠后的

private fun notification() {
        val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        val builder: NotificationCompat.Builder?
        val channel = NotificationChannel(
            "通知渠道ID",
            "通知渠道名称", NotificationManager.IMPORTANCE_DEFAULT
        )
        channel.description = "通知渠道描述"
        mNotificationManager.createNotificationChannel(channel)
        builder = NotificationCompat.Builder(this, "通知渠道ID")
        builder.setContentText("可可托海的牧羊人-王琪")
        builder.setSmallIcon(R.drawable.ic_baseline_music_note_24)
        builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_baseline_music_note_24))
        builder.priority = NotificationCompat.PRIORITY_HIGH
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.google.cn/"))
        val pIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_IMMUTABLE)
        builder.setContentIntent(pIntent)

        val remoteViews = RemoteViews(this.packageName, R.layout.remote_view)
        builder.setCustomBigContentView(remoteViews)
        builder.setCustomHeadsUpContentView(remoteViews)


        builder.setDefaults(NotificationCompat.DEFAULT_ALL)
        val notification = builder.build()
        mNotificationManager.notify(0, notification)

    }

折叠状态

20210313_145724_20

展开状态

20210313_145800_28

其他目标版本为 Android 12的行为变化,请 参考官网

-------------本文结束感谢您的阅读-------------
if (本文对您有用) { Pay (请随意¥打赏) } else { Commit feedback (底部评论区提交建议、反馈) } 感谢支持!
0%