Skip to content

Commit 8c04ec2

Browse files
authored
Check for notification_open duplicate logging based on message ID. (#6112)
- Changed to use the message ID when checking for duplicate notification_open logging instead of the Activity's Intent. This should avoid logging again when an app starts another activity using a copy of the notification's intent (#6091).
1 parent 9ed73c4 commit 8c04ec2

File tree

3 files changed

+61
-54
lines changed

3 files changed

+61
-54
lines changed

firebase-messaging/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Unreleased
22
* [changed] Retry Topic Subscribe/Unsubscribe operations with exponential
33
backoff if they hit a quota error.
4+
* [changed] Checked for notification_open duplicate logging based on message ID
5+
instead of the Activity's Intent.
46

57
# 24.0.0
68
* [changed] Switched Firelog to use the new TransportBackend.

firebase-messaging/src/main/java/com/google/firebase/messaging/FcmLifecycleCallbacks.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,25 @@
2424
import android.os.Bundle;
2525
import android.os.Handler;
2626
import android.os.Looper;
27+
import android.text.TextUtils;
2728
import android.util.Log;
28-
import java.util.Collections;
29-
import java.util.Set;
30-
import java.util.WeakHashMap;
29+
import java.util.ArrayDeque;
30+
import java.util.Queue;
3131

3232
class FcmLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
3333

34-
/** Keep a list of intents that we've seen to avoid accidentally logging events twice. */
35-
private final Set<Intent> seenIntents =
36-
Collections.newSetFromMap(new WeakHashMap<Intent, Boolean>());
34+
private static final int RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE = 10;
35+
36+
/** Last N message IDs that have been logged to prevent duplicate logging. */
37+
private final Queue<String> recentlyLoggedMessageIds =
38+
new ArrayDeque<>(RECENTLY_LOGGED_MESSAGE_IDS_MAX_SIZE);
3739

3840
// TODO(b/258424124): Migrate to go/firebase-android-executors
3941
@SuppressLint("ThreadPoolCreation")
4042
@Override
4143
public void onActivityCreated(Activity createdActivity, Bundle instanceState) {
4244
Intent startingIntent = createdActivity.getIntent();
43-
if (startingIntent == null || !seenIntents.add(startingIntent)) {
44-
// already seen (and logged) this, no need to go any further.
45+
if (startingIntent == null) {
4546
return;
4647
}
4748

@@ -57,12 +58,7 @@ public void onActivityCreated(Activity createdActivity, Bundle instanceState) {
5758
}
5859

5960
@Override
60-
public void onActivityPaused(Activity pausedActivity) {
61-
if (pausedActivity.isFinishing()) {
62-
// iff the activity is finished we can remove the intent from our "seen" list
63-
seenIntents.remove(pausedActivity.getIntent());
64-
}
65-
}
61+
public void onActivityPaused(Activity pausedActivity) {}
6662

6763
@Override
6864
public void onActivityDestroyed(Activity destroyedActivity) {}
@@ -84,6 +80,14 @@ private void logNotificationOpen(Intent startingIntent) {
8480
try {
8581
Bundle extras = startingIntent.getExtras();
8682
if (extras != null) {
83+
String messageId = MessagingAnalytics.getMessageId(extras);
84+
if (!TextUtils.isEmpty(messageId)) {
85+
if (recentlyLoggedMessageIds.contains(messageId)) {
86+
// Already logged, don't log again.
87+
return;
88+
}
89+
recentlyLoggedMessageIds.add(messageId);
90+
}
8791
analyticsData = extras.getBundle(Constants.MessageNotificationKeys.ANALYTICS_DATA);
8892
}
8993
} catch (RuntimeException e) {

firebase-messaging/src/test/java/com/google/firebase/messaging/FirebaseMessagingServiceRoboTest.java

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@
5858
import com.google.android.gms.cloudmessaging.CloudMessage;
5959
import com.google.android.gms.cloudmessaging.Rpc;
6060
import com.google.common.collect.ImmutableSet;
61-
import com.google.common.collect.Sets;
6261
import com.google.firebase.FirebaseApp;
6362
import com.google.firebase.FirebaseOptions;
6463
import com.google.firebase.iid.FirebaseInstanceIdReceiver;
6564
import com.google.firebase.messaging.AnalyticsTestHelper.Analytics;
65+
import com.google.firebase.messaging.Constants.MessagePayloadKeys;
6666
import com.google.firebase.messaging.testing.AnalyticsValidator;
6767
import com.google.firebase.messaging.testing.Bundles;
6868
import com.google.firebase.messaging.testing.FakeConnectorComponent;
@@ -105,6 +105,8 @@ public class FirebaseMessagingServiceRoboTest {
105105
// blank activity
106106
public static class MyTestActivity extends Activity {}
107107

108+
public static class MySecondTestActivity extends Activity {}
109+
108110
private static final AnalyticsValidator analyticsValidator =
109111
FakeConnectorComponent.getAnalyticsValidator();
110112

@@ -501,6 +503,43 @@ public void testNotification_clickAnalytics_recreateActivity() throws Exception
501503
}
502504
}
503505

506+
/** Test that a notification does not re-log events when the same notification is found. */
507+
@Test
508+
public void notification_clickAnalytics_duplicateMessageId() throws Exception {
509+
FirebaseMessaging.getInstance(firebaseApp); // register activity lifecycle friends
510+
simulateNotificationMessageWithAnalytics();
511+
512+
PendingIntent clickPendingIntent = getSingleShownNotification().contentIntent;
513+
try (ActivityScenario<MyTestActivity> scenario =
514+
dispatchActivityIntentToActivity(clickPendingIntent)) {
515+
Intent secondActivityIntent = new Intent();
516+
secondActivityIntent.putExtras(shadowOf(clickPendingIntent).getSavedIntent());
517+
// Start another Activity with the same notification and check it doesn't log open again.
518+
try (ActivityScenario<MySecondTestActivity> secondScenario =
519+
ActivityScenario.launch(secondActivityIntent)) {
520+
List<AnalyticsValidator.LoggedEvent> events = analyticsValidator.getLoggedEvents();
521+
assertThat(events).hasSize(2);
522+
AnalyticsValidator.LoggedEvent receiveEvent = events.get(0);
523+
assertThat(receiveEvent.getOrigin()).isEqualTo(Analytics.ORIGIN_FCM);
524+
assertThat(receiveEvent.getName()).isEqualTo(Analytics.EVENT_NOTIFICATION_RECEIVE);
525+
assertThat(receiveEvent.getParams())
526+
.string(Analytics.PARAM_MESSAGE_ID)
527+
.isEqualTo("composer_key");
528+
assertThat(receiveEvent.getParams())
529+
.string(Analytics.PARAM_MESSAGE_NAME)
530+
.isEqualTo("composer_label");
531+
assertThat(receiveEvent.getParams())
532+
.integer(Analytics.PARAM_MESSAGE_TIME)
533+
.isEqualTo(1234567890);
534+
assertThat(receiveEvent.getParams()).doesNotContainKey(Analytics.PARAM_TOPIC);
535+
536+
AnalyticsValidator.LoggedEvent openEvent = events.get(1);
537+
assertThat(openEvent.getOrigin()).isEqualTo(Analytics.ORIGIN_FCM);
538+
assertThat(openEvent.getName()).isEqualTo(Analytics.EVENT_NOTIFICATION_OPEN);
539+
}
540+
}
541+
}
542+
504543
/** Test that a notification logs the correct event on dismiss. */
505544
@Test
506545
public void testNotification_dismissAnalytics() throws Exception {
@@ -612,6 +651,7 @@ private void simulateNotificationMessageWithAnalytics() throws InterruptedExcept
612651
// Robo manifest doesn't have a default activity, so set click action so that we get a
613652
// click pending intent.
614653
builder.addData(DisplayNotificationRoboTest.KEY_CLICK_ACTION, "click_action");
654+
builder.addData(MessagePayloadKeys.MSGID, "msg_id");
615655
AnalyticsTestHelper.addAnalyticsExtras(builder);
616656
startServiceViaReceiver(builder.buildIntent());
617657
}
@@ -766,43 +806,4 @@ private static Set<ServiceConnection> getBoundServiceConnections() {
766806
.getBoundServiceConnections();
767807
return ImmutableSet.copyOf(connList.toArray(new ServiceConnection[0]));
768808
}
769-
770-
/**
771-
* Calls handleIntent on {@link #service} in a background thread and connects any outgoing
772-
* bindService calls made in the meantime.
773-
*
774-
* <p>Returns a CountDownLatch that's finished when the call to handleIntent finishes.
775-
*/
776-
private CountDownLatch handleIntent(Intent intent, int time, TimeUnit unit) throws Exception {
777-
long timeoutAtMillis = System.currentTimeMillis() + unit.toMillis(time);
778-
779-
// Connections that were active before we started
780-
Set<ServiceConnection> previousConnections = getBoundServiceConnections();
781-
782-
// Run the thing that will cause a bindService call in the background
783-
CountDownLatch finishedLatch = new CountDownLatch(1);
784-
executorService.execute(
785-
() -> {
786-
service.handleIntent(intent);
787-
finishedLatch.countDown();
788-
});
789-
790-
// wait for the call to be made (service connection to appear)
791-
while (Sets.difference(getBoundServiceConnections(), previousConnections).isEmpty()) {
792-
assertWithMessage("timed out waiting for binder connection")
793-
.that(System.currentTimeMillis())
794-
.isLessThan(timeoutAtMillis);
795-
796-
TimeUnit.MILLISECONDS.sleep(50);
797-
}
798-
799-
CountDownLatch flushLatch = new CountDownLatch(1);
800-
new Handler(Looper.getMainLooper()).post(flushLatch::countDown);
801-
shadowOf(Looper.getMainLooper()).idle();
802-
803-
long millisRemaining = Math.max(0, timeoutAtMillis - System.currentTimeMillis());
804-
assertThat(flushLatch.await(millisRemaining, TimeUnit.MILLISECONDS)).isTrue();
805-
806-
return finishedLatch;
807-
}
808809
}

0 commit comments

Comments
 (0)