Week 8 - Firebase

Introduction to Firebase

firebase is a framework that provides backend data capabilities:

cloud messaging, user authentication, etc

Firebase cloud messaging

A capability that sends messages to android devices that have your firebase enabled app installed

Firebase Cloud Messaging (FCM) 是一个跨平台的消息传递解决方案,可以让你可靠地发送通知和消息到Android、iOS和网页应用上。

在FCM中,”system tray”通常指的是Android设备上的通知栏。当应用程序在后台或者完全关闭的状态下收到通知时,这些通知会显示在设备的系统托盘中,也就是屏幕顶部的下拉通知栏。用户可以在这里看到各种通知信息,并且可以从系统托盘中直接打开对应的通知,通常会打开应用程序的某个特定界面或执行某些操作。

具体到FCM,这意味着当一个通知消息被发送到一个设备并且该应用程序不在前台运行时,这个通知将会显示在系统托盘中,而不是直接通过应用程序打开一个界面或活动(Activity)。这种类型的通知被称为“通知消息”(notification message),与“数据消息”(data message)相对,后者即使应用在后台也可以被应用程序的代码接收和处理。

Untitled

instance ID

Identifies a single app instance.

Developer需要去记录和track instanceID

开发者需要记录和跟踪 Instance ID(或现在通常称为FCM注册令牌)的原因主要是为了实现目标推送。这意味着,当你想要向特定的用户或设备发送消息时,你需要知道它们的注册令牌。例如,如果你正在开发一个即时消息应用,你需要能够向特定用户发送消息,这时你就需要知道用户设备的令牌。

记录和跟踪令牌通常涉及以下步骤:

  1. 获取和存储令牌:当应用在用户设备上安装并首次运行时,FCM SDK 会生成一个注册令牌。你的应用需要监听令牌生成事件,并将其上传到你的服务器。
  2. 更新令牌:令牌可能会因为应用重新安装、用户清除应用数据、操作系统更新等原因更改。FCM SDK 会在令牌更改时通知你的应用。你的应用需要监听这些更改,并更新服务器上的记录。
  3. 维护令牌关联:你需要确保用户的账户信息和注册令牌之间的映射是最新的。这通常意味着在用户登录、登出、注册设备或更改设备时更新服务器上的记录。
  4. 安全性:因为令牌是向特定设备发送信息的关键,所以必须确保这些数据的传输和存储是安全的。

关于新的令牌管理系统,Firebase 逐渐推出了一些改进,以简化令牌管理过程。这些改进可能包括自动令牌刷新和简化的API来处理令牌。Firebase建议使用FCM SDK的最新版本,以确保你的应用可以利用这些改进。

InstanceID 被用来获取FCM注册令牌,但是现在通常推荐的做法是直接使用由 FirebaseMessaging 服务提供的令牌。**FirebaseInstanceId** 类和相关的API已经被弃用,因为FirebaseMessaging服务现在提供了获取和刷新令牌的功能。

InstanceID 提供了一种在应用实例之间安全地识别设备的方式。每当一个客户端应用实例注册到接收FCM消息时,它都会获得一个唯一的ID,即 **InstanceID**。这个 InstanceID 表示的是一个应用实例在一个设备上的身份,并且通常与一个应用的注册令牌(registration token)相关联。

每一个app有一个instanceID

![Untitled 1](/2024/02/26/CS5520-Week8/Untitled 1.png)

Database for Firebase Realtime Database

Firebase提供了两种主要的数据库选项:Cloud Firestore和Firebase Realtime Database。它们都是NoSQL数据库,用于存储和同步客户端和服务器之间的数据,但它们在数据模型、查询能力、数据同步等方面有显著的不同。

以下是两者的主要区别:

Cloud Firestore:

  1. 数据模型:Firestore是一个文档-集合数据库。数据被存储在文档中,文档再被组织在集合中。每个文档可以包含复杂的数据类型,并且可以包含子集合。
  2. 查询:Firestore支持更复杂的查询。你可以进行多字段排序和复合查询,而不需要在客户端进行额外的过滤。
  3. 扩展:Firestore是设计来自动扩展的,它适用于更大规模的应用程序。
  4. 离线支持:Firestore提供了内置的离线支持。即使设备没有网络连接,应用程序也可以从缓存中读取和写入数据。
  5. 价格:计费基于读取、写入和删除操作的次数,以及存储的数据量和网络带宽。
  6. 数据一致性和事务:Firestore提供了强一致性的读写操作和复杂的事务支持。

Firebase Realtime Database:

  1. 数据模型:Realtime Database是一个大的JSON树。它非常适合简单的数据,但不适合层次结构很深的数据。
  2. 查询:查询功能较基础。如果你需要进行复杂的查询,可能需要在客户端进行额外的数据处理。
  3. 实时同步:Realtime Database的特点是可以快速同步数据。所有的客户端都可以几乎实时地看到数据的更新。
  4. 离线支持:虽然也支持离线操作,但是需要手动配置和管理。
  5. 价格:计费基于网络带宽和存储的数据量,而不是操作的次数。
  6. 数据一致性和事务:提供了基本的事务支持,数据一致性在分布式系统中可能稍微弱一些。

选择哪一个?
选择使用Firestore还是Realtime Database取决于你的特定需求。如果你需要更复杂的查询和更强的数据结构,或者你的应用程序需要自动扩展以支持更多的用户,Firestore可能是更好的选择。如果你的应用程序需要高性能的实时同步,Realtime Database可能更合适。

Code Demo

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package edu.neu.madcourse.firebasedemo.realtimedatabase;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.google.android.gms.tasks.Task;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;

import edu.neu.madcourse.firebasedemo.R;
import edu.neu.madcourse.firebasedemo.realtimedatabase.models.User;

/**
* Here, we will demonstrate the use of Firebase Realtime DB which syncs your data across different
* devices that are accessing the same DB in realtime.
*/
public class RealtimeDatabaseActivity extends AppCompatActivity {

private static final String TAG = RealtimeDatabaseActivity.class.getSimpleName();

private FirebaseDatabase mDatabase;
private TextView user1;
private TextView score_user1;
private TextView user2;
private TextView score_user2;
private RadioButton player;

@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_realtime_database);

user1 = (TextView) findViewById(R.id.username1);
user2 = (TextView) findViewById(R.id.username2);
score_user1 = (TextView) findViewById(R.id.score1);
score_user2 = (TextView) findViewById(R.id.score2);
player = (RadioButton) findViewById(R.id.player1);

// Connect with firebase
mDatabase = FirebaseDatabase.getInstance();
// The documentation mentions that you do not need to add the URL if your location is
// us-central1, but at times it does not work. If it does not work, then add the url of your
// db in the getInstance() call. eg: getInstance("https://testfirebase-default-rtdb.firebaseio.com/")

// Update the score in realtime
mDatabase.getReference().child("users").addChildEventListener(
new ChildEventListener() {

@Override
public void onChildAdded(@NonNull DataSnapshot dataSnapshot, String s) {
showScore(dataSnapshot);
Log.e(TAG, "onChildAdded: dataSnapshot = " + dataSnapshot.getValue().toString());
}

@Override
public void onChildChanged(@NonNull DataSnapshot dataSnapshot, String s) {
showScore(dataSnapshot);
Log.v(TAG, "onChildChanged: " + dataSnapshot.getValue().toString());
}

@Override
public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {

}

@Override
public void onChildMoved(@NonNull DataSnapshot dataSnapshot, String s) {

}

@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
Log.e(TAG, "onCancelled:" + databaseError);
Toast.makeText(getApplicationContext()
, "DBError: " + databaseError, Toast.LENGTH_SHORT).show();
}
}
);
}

// Add 5 points Button
public void addFivePoints(View view) {
RealtimeDatabaseActivity.this.onAddScore(mDatabase.getReference(), player.isChecked() ? "user1" : "user2");
}

/**
* This method resets the scores of both the players back to 0 on the DB as well as the app.
*
* @param view the button reference from the XML file.
*/
public void resetUsers(View view) {

User user;
user = new User("user1", "0");
Task<Void> t1 = mDatabase.getReference().child("users").child(user.username).setValue(user);

user = new User("user2", "0");
Task<Void> t2 = mDatabase.getReference().child("users").child(user.username).setValue(user);

if(!t1.isSuccessful() && !t2.isSuccessful()){
Toast.makeText(getApplicationContext(),"Unable to reset players!",Toast.LENGTH_SHORT).show();
}
else if(!t1.isSuccessful() && t2.isSuccessful()){
Toast.makeText(getApplicationContext(),"Unable to reset player1!",Toast.LENGTH_SHORT).show();
}
else if(t1.isSuccessful() && t2.isSuccessful()){
Toast.makeText(getApplicationContext(),"Unable to reset player2!",Toast.LENGTH_SHORT).show();
}

}

// Add data to firebase button
public void doAddDataToDb(View view) {
// Write a message to the database
DatabaseReference myRef = mDatabase.getReference("message");

Task<Void> t = myRef.setValue("Hello, World!");

t.addOnCompleteListener(task -> {
if(!t.isSuccessful()){
Toast.makeText(getApplicationContext()
, "Failed to write value into firebase. " , Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext()
, "Write Successful." , Toast.LENGTH_SHORT).show();
}
});

// Read from the database by listening for a change to that item.
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
// This method is called once with the initial value and again
// whenever data at this location is updated.
String value = dataSnapshot.getValue(String.class);
Log.d(TAG, "Value is: " + value);
TextView tv = (TextView) findViewById(R.id.dataUpdateTextView);
tv.setText(value);
}

@Override
public void onCancelled(@NonNull DatabaseError error) {
// Failed to read value
Log.w(TAG, "Failed to read value.", error.toException());
Toast.makeText(getApplicationContext()
, "Failed to write value into firebase. " , Toast.LENGTH_SHORT).show();
}

});

}

/**
* This method represents adding 5 points to the score of the specified player.
*
* @param postRef a reference of the database
* @param user String specifying whether it is user 1 or 2
*/
private void onAddScore(DatabaseReference postRef, String user) {
postRef
.child("users")
.child(user)
.runTransaction(new Transaction.Handler() {
@NonNull
@Override
public Transaction.Result doTransaction(@NonNull MutableData mutableData) {

User user = mutableData.getValue(User.class);

if (user == null) {
return Transaction.success(mutableData);
}

user.score = String.valueOf(Integer.parseInt(user.score) + 5);

mutableData.setValue(user);

return Transaction.success(mutableData);
}

@Override
public void onComplete(DatabaseError databaseError, boolean successful,
DataSnapshot dataSnapshot) {
// Transaction completed
Log.d(TAG, "postTransaction:onComplete:" + databaseError);
if (successful) {
Toast.makeText(RealtimeDatabaseActivity.this,
"Score Updated", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(RealtimeDatabaseActivity.this
, "DBError: " + databaseError, Toast.LENGTH_SHORT).show();
}
}

});
}

/**
* Displays the score using the DataSnapshot object by getting the "User" object corresponding
* values from it.
*
* @param dataSnapshot Immutable copies of the data at a Firebase Database location.
*/
private void showScore(DataSnapshot dataSnapshot) {
//Getting the user object corresponding value from the dataSnapshot
User user = dataSnapshot.getValue(User.class);

if (dataSnapshot.getKey() != null) {
//Appropriately updating the score of the user by querying on the name of the user.
if (dataSnapshot.getKey().equalsIgnoreCase("user1")) {
score_user1.setText(String.valueOf(user.score));
user1.setText(user.username);
} else {
score_user2.setText(String.valueOf(user.score));
user2.setText(user.username);
}
}
}

}

这段代码是使用Firebase Realtime Database的Android客户端库进行实时数据同步的示例。它展示了如何连接到Firebase数据库,并且设置了一个监听器来监听数据的变化。以下是代码逐行解释:

1
2
3
// Connect with firebase
mDatabase = FirebaseDatabase.getInstance();

这行代码获取了 FirebaseDatabase 的一个实例。这是与Firebase Realtime Database进行交互的入口点。

1
2
3
4
// The documentation mentions that you do not need to add the URL if your location is
// us-central1, but at times it does not work. If it does not work, then add the url of your
// db in the getInstance() call. eg: getInstance("<https://testfirebase-default-rtdb.firebaseio.com/>")

这段注释解释了通常情况下如果你的数据库位于us-central1(一个常用的Firebase地区),你不需要在 getInstance() 方法中提供数据库的URL。但是如果默认的方式不起作用,你可能需要直接提供数据库的完整URL。

1
2
3
// Update the score in realtime
mDatabase.getReference().child("users").addChildEventListener(

这行代码设置了一个指向数据库中 "users" 节点的引用,并为它添加了一个 ChildEventListener。这意味着任何在 "users" 节点下的子节点发生的变化都会通知这个监听器。

1
2
new ChildEventListener() {

这行代码创建了 ChildEventListener 的一个匿名类实例。

1
2
3
4
5
6
@Override
public void onChildAdded(@NonNull DataSnapshot dataSnapshot, String s) {
showScore(dataSnapshot);
Log.e(TAG, "onChildAdded: dataSnapshot = " + dataSnapshot.getValue().toString());
}

当一个新的子节点被添加到 "users" 下时,onChildAdded 方法会被调用。它接收一个 DataSnapshot 对象,包含了被添加的子节点的数据。这里它调用了一个 showScore 方法(未在代码中显示)来处理新添加的数据,并且使用 Log.e 记录了一个错误级别的日志。

1
2
3
4
5
6
@Override
public void onChildChanged(@NonNull DataSnapshot dataSnapshot, String s) {
showScore(dataSnapshot);
Log.v(TAG, "onChildChanged: " + dataSnapshot.getValue().toString());
}

"users" 下的某个子节点的数据发生变化时,onChildChanged 方法会被调用。同样地,它使用 DataSnapshot 来获取变化的数据,并调用 showScore 方法来处理变化的数据,然后记录一个详细级别的日志。

1
2
3
4
5
@Override
public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {

}

"users" 下的某个子节点被移除时,onChildRemoved 方法会被调用。这里没有实现具体的操作。

1
2
3
4
5
@Override
public void onChildMoved(@NonNull DataSnapshot dataSnapshot, String s) {

}

如果子节点在列表中的位置发生了变化,onChildMoved 方法会被调用。这通常发生在节点的某个排序属性被修改时。同样,这里没有实现具体的操作。

1
2
3
4
5
6
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
Log.e(TAG, "onCancelled:" + databaseError);
Toast.makeText(getApplicationContext(), "DBError: " + databaseError, Toast.LENGTH_SHORT).show();
}

如果监听过程中发生了错误,onCancelled 方法会被调用。这里它记录了一个错误并显示了一个Toast消息给用户。

整体来说,这段代码演示了如何在Android应用中使用Firebase Realtime Database来监听 users 节点下子节点的增加和变化,并在数据变化时进行一些处理。

在这段代码中,**.child().child()** 方法是用来定位和操作 Firebase Realtime Database 中的特定路径或节点的。

  • .child("users") 定位到数据库中名为 "users" 的节点。这通常是一个顶级节点,包含了所有用户相关的信息。
  • .child(user) 进一步定位到 "users" 节点下特定用户名对应的子节点。这里的 user 参数应该是一个字符串,表示特定用户的唯一标识符或用户名。

Notification

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
72
73
74
75
76
77
78
79
package edu.neu.madcourse.firebasedemo.notification;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import edu.neu.madcourse.firebasedemo.FakeCallActivity;
import edu.neu.madcourse.firebasedemo.R;

/*
* Upgrade the method based on the latest Android version
*/

public class SendNotificationActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

createNotificationChannel();

setContentView(R.layout.activity_send_notification);

}

public void createNotificationChannel() {
// This must be called early because it must be called before a notification is sent.
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name);
String description = getString(R.string.channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(getString(R.string.channel_id), name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}

public void sendNotification(View view) {

// Prepare intent which is triggered if the
// notification is selected
Intent intent = new Intent(this, ReceiveNotificationActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(this, (int) System.currentTimeMillis(), intent, 0);
PendingIntent callIntent = PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
new Intent(this, FakeCallActivity.class), 0);

// Build notification
// Need to define a channel ID after Android Oreo
String channelId = getString(R.string.channel_id);
NotificationCompat.Builder notifyBuild = new NotificationCompat.Builder(this, channelId)
//"Notification icons must be entirely white."
.setSmallIcon(R.drawable.foo)
.setContentTitle("New mail from " + "test@test.com")
.setContentText("Subject")
.setPriority(NotificationCompat.PRIORITY_HIGH)
// hide the notification after its selected
.setAutoCancel(true)
.addAction(R.drawable.foo, "Call", callIntent)
.setContentIntent(pIntent);

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// // notificationId is a unique int for each notification that you must define
notificationManager.notify(0, notifyBuild.build());

}
}

在Android O(API 级别 26)及更高版本中,NotificationChannel(通知渠道)是一种重要的概念,它为用户提供了更丰富的控制通知的能力。通过使用通知渠道,应用可以将通知分组,并允许用户对这些不同类型的通知进行细致的控制。这意味着用户可以根据自己的需要调整每个通知渠道的重要性级别、声音、振动等设置。

为什么需要NotificationChannel

随着应用越来越复杂,它们需要向用户发送多种类型的通知,如消息提醒、进度更新、动作提示等。在Android O之前,所有的通知设置都是全局应用的,用户无法根据通知的类型进行个性化设置。NotificationChannel的引入解决了这个问题,使用户能够根据自己的偏好来管理通知。

如何使用NotificationChannel

  1. 创建通知渠道:当应用运行在Android O及更高版本上时,你需要创建一个或多个NotificationChannel实例,并将其提交给系统。每个通知渠道都有一个唯一的ID和用户可见的名称。
1
2
3
4
5
6
7
8
9
10
11
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "My Channel";
String description = "Channel Description";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// 在NotificationManager中注册通知渠道
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}

  1. 发送通知:创建通知时,指定通知要使用的通知渠道ID。
1
2
3
4
5
6
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT);

  1. 用户控制:用户可以进入系统设置,查看应用的每个通知渠道,并根据需要调整通知的行为(如关闭某个通知渠道的声音)。

重要性

  • NotificationChannel提供了向后兼容的解决方案。对于运行在Android O以下版本的设备,应用仍然可以发送通知,但无需创建通知渠道。
  • 一旦创建,通知渠道的某些设置就不能通过应用程序更改,只能由用户在系统设置中更改。因此,应用应当谨慎地规划其通知渠道的策略。

通过引入NotificationChannel,Android增强了通知系统的灵活性和用户控制能力,提升了用户体验。

这段代码是用于在Android应用中创建和触发一个通知的。以下是代码的逐行解释:

  1. 创建NotificationBuilder对象:
1
2
Notification noti = new NotificationCompat.Builder(context: this, channelId)

这里使用了NotificationCompat.Builder构造函数来创建一个新的通知构建器对象。它接收两个参数:一个是上下文(this通常指的是当前的ActivityApplication),另一个是通知渠道的ID。从Android O开始,所有通知都必须通过一个通知渠道发送。

  1. 设置通知的各种属性:
1
2
3
4
5
.setContentTitle("Notified! " + Integer.toString(notificationGeneration++))
.setContentText("Subject Text")
.setSmallIcon(R.drawable.foo)
.setTicker("Ticker text")

这部分代码设置了通知的标题、文本内容、小图标和滚动文字(在状态栏上显示的一小段文本)。

  1. 添加动作:
1
2
3
.addAction(R.drawable.foo, title: "Call", callIntent)
.addAction(R.drawable.thinking_face, title: "More", moreIntent)

addAction方法用于在通知中添加动作按钮。每个动作都有一个图标、标题和一个Intent,当用户点击这个动作时,该Intent会被触发。

  1. 设置内容Intent:
1
2
.setContentIntent(moreIntent).build();

这里为通知设置了一个Intent,当用户点击通知本身时会触发这个Intent

  1. 获取NotificationManager服务:
1
2
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

这行代码从系统服务中获取NotificationManager实例,这个服务负责展示所有的通知。

  1. 设置通知标志:
1
2
noti.flags |= Notification.FLAG_AUTO_CANCEL;

通过设置FLAG_AUTO_CANCEL标志,这个通知在用户点击后会自动被取消。

  1. 发送通知:
1
2
notificationManager.notify(NOTIFICATION_UNIQUE_ID, noti);

最后,notify方法用于触发通知。它接收一个唯一的ID(这里是NOTIFICATION_UNIQUE_ID)和Notification对象。这个唯一ID允许应用更新或取消这个特定的通知。

注释部分还提到了一个备选方案,即使用一个唯一的ID生成策略(在这个例子中是notificationGeneration变量)来确保每个通知都有不同的ID,这样新的通知不会覆盖旧的通知。这通常在你需要在同一时间显示多个通知时很有用。

整体来说,这段代码演示了如何在Android应用中创建一个具有基本属性和动作的通知,并通过NotificationManager将其显示给用户。

在Android中,Notification对象有一个flags字段,它是一个整数(int),用于设置通知的行为。这个字段可以接受多个标志的组合,这些标志是Notification类中定义的常量。

FLAG_AUTO_CANCELNotification类中定义的一个标志,当这个标志被设置时,它会指示系统在用户点击了通知之后自动将其取消(关闭)。这意味着通知将不再显示在通知栏中。

|=是Java中的按位或赋值运算符,它用于将特定的位设置为1。如果你想在不改变其他位的情况下添加一个标志,你可以使用这个运算符。下面是如何使用|=运算符来设置FLAG_AUTO_CANCEL标志的示例:

1
2
noti.flags |= Notification.FLAG_AUTO_CANCEL;

这行代码的执行过程如下:

  1. 读取noti.flags当前的值。
  2. 执行按位或运算(|),将noti.flags的值与Notification.FLAG_AUTO_CANCEL的值结合起来。如果FLAG_AUTO_CANCEL中对应的位是1,那么在结果中该位也会被设置为1,这样FLAG_AUTO_CANCEL就被添加到了flags字段中。
  3. 将新值赋回noti.flags

通过这种方式,即使noti.flags中已经设置了其他的标志,使用|=也不会影响它们,只会添加新的标志。这是一种在不改变现有位的情况下设置特定位的常用技术。

所以,|=用于设置FLAG_AUTO_CANCEL,它不是自动取消的原因,而是用于在不覆盖现有标志的情况下添加这个标志的方法。FLAG_AUTO_CANCEL本身定义了通知在点击后应该自动被取消的行为。