Commit 38d62c24 authored by Mohamed Amir Yosef's avatar Mohamed Amir Yosef Committed by Commit Bot

[FCM] Persist/Read FCM messages when Chrome is in the back/fore-ground

This CL puts all pieces together making sure that received messages
get persisted if Chrome is in the background, and replayed next
time it comes to the foreground.

Bug: 882887
Change-Id: I3dea0aa3ad9a01a7f4d59c5b798a018488980beb
Reviewed-on: https://chromium-review.googlesource.com/c/1276608
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Reviewed-by: default avatarPeter Beverloo <peter@chromium.org>
Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#608397}
parent 4cd69a24
...@@ -12,6 +12,7 @@ import android.text.TextUtils; ...@@ -12,6 +12,7 @@ import android.text.TextUtils;
import com.google.android.gms.gcm.GcmListenerService; import com.google.android.gms.gcm.GcmListenerService;
import com.google.ipc.invalidation.ticl.android2.channel.AndroidGcmController; import com.google.ipc.invalidation.ticl.android2.channel.AndroidGcmController;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
...@@ -23,6 +24,7 @@ import org.chromium.components.background_task_scheduler.TaskIds; ...@@ -23,6 +24,7 @@ import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.background_task_scheduler.TaskInfo; import org.chromium.components.background_task_scheduler.TaskInfo;
import org.chromium.components.gcm_driver.GCMDriver; import org.chromium.components.gcm_driver.GCMDriver;
import org.chromium.components.gcm_driver.GCMMessage; import org.chromium.components.gcm_driver.GCMMessage;
import org.chromium.components.gcm_driver.LazySubscriptionsManager;
/** /**
* Receives Downstream messages and status of upstream messages from GCM. * Receives Downstream messages and status of upstream messages from GCM.
...@@ -87,13 +89,25 @@ public class ChromeGcmListenerService extends GcmListenerService { ...@@ -87,13 +89,25 @@ public class ChromeGcmListenerService extends GcmListenerService {
} }
/** /**
* Either schedules |message| to be dispatched through the Job Scheduler, which we use on * If Chrome is backgrounded, messages coming from lazy subscriptions are
* Android N and beyond, or immediately dispatches the message on other versions of Android. * persisted on disk and replayed next time Chrome is forgrounded. If Chrome is forgrounded or
* Must be called on the UI thread both for the BackgroundTaskScheduler and for dispatching * if the message isn't coming from a lazy subscription, this method either schedules |message|
* the |message| to the GCMDriver. * to be dispatched through the Job Scheduler, which we use on Android N and beyond, or
* immediately dispatches the message on other versions of Android. Must be called on the UI
* thread both for the BackgroundTaskScheduler and for dispatching the |message| to the
* GCMDriver.
*/ */
static void scheduleOrDispatchMessageToDriver(GCMMessage message) { static void scheduleOrDispatchMessageToDriver(GCMMessage message) {
ThreadUtils.assertOnUiThread(); ThreadUtils.assertOnUiThread();
final String subscriptionId = LazySubscriptionsManager.buildSubscriptionUniqueId(
message.getAppId(), message.getSenderId());
if (!ApplicationStatus.hasVisibleActivities()
&& LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)) {
// TODO(https://crbug.com/882887): record a UMA metric for how long
// does it take to check if the subscription is lazy.
LazySubscriptionsManager.persistMessage(subscriptionId, message);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Bundle extras = message.toBundle(); Bundle extras = message.toBundle();
......
...@@ -12,6 +12,7 @@ import org.chromium.base.annotations.JNINamespace; ...@@ -12,6 +12,7 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
import java.io.IOException; import java.io.IOException;
import java.util.Set;
/** /**
* This class is the Java counterpart to the C++ GCMDriverAndroid class. * This class is the Java counterpart to the C++ GCMDriverAndroid class.
...@@ -47,6 +48,23 @@ public class GCMDriver { ...@@ -47,6 +48,23 @@ public class GCMDriver {
throw new IllegalStateException("Already instantiated"); throw new IllegalStateException("Already instantiated");
} }
sInstance = new GCMDriver(nativeGCMDriverAndroid); sInstance = new GCMDriver(nativeGCMDriverAndroid);
// Don't bother to read the stored messages unless there are actually
// messages persisted on disk. Calling
// LazySubscriptionsManager.hasPersistedMessages() should be a cheap way
// to avoid unnecessary disk reads.
if (LazySubscriptionsManager.hasPersistedMessages()) {
// TODO(https://crbug.com/882887): record a UMA metric for how long
// does it take to read all persisted messaged.
Set<String> lazySubscriptionIds = LazySubscriptionsManager.getLazySubscriptionIds();
for (String id : lazySubscriptionIds) {
GCMMessage[] messages = LazySubscriptionsManager.readMessages(id);
for (GCMMessage message : messages) {
dispatchMessage(message);
}
LazySubscriptionsManager.deletePersistedMessagesForSubscriptionId(id);
}
LazySubscriptionsManager.storeHasPersistedMessages(/*hasPersistedMessages=*/false);
}
return sInstance; return sInstance;
} }
......
...@@ -13,6 +13,7 @@ import org.json.JSONObject; ...@@ -13,6 +13,7 @@ import org.json.JSONObject;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
...@@ -29,6 +30,7 @@ import java.util.Set; ...@@ -29,6 +30,7 @@ import java.util.Set;
public class LazySubscriptionsManager { public class LazySubscriptionsManager {
private static final String TAG = "LazySubscriptions"; private static final String TAG = "LazySubscriptions";
private static final String FCM_LAZY_SUBSCRIPTIONS = "fcm_lazy_subscriptions"; private static final String FCM_LAZY_SUBSCRIPTIONS = "fcm_lazy_subscriptions";
private static final String HAS_PERSISTED_MESSAGES_KEY = "has_persisted_messages";
private static final String PREF_PACKAGE = private static final String PREF_PACKAGE =
"org.chromium.components.gcm_driver.lazy_subscriptions"; "org.chromium.components.gcm_driver.lazy_subscriptions";
...@@ -41,6 +43,33 @@ public class LazySubscriptionsManager { ...@@ -41,6 +43,33 @@ public class LazySubscriptionsManager {
// shouldn't be instantiated. // shouldn't be instantiated.
private LazySubscriptionsManager() {} private LazySubscriptionsManager() {}
/**
* Stores a global flag that indicates whether there are any persisted
* messages to read. The flag could be read using hasPersistedMessages().
* @param hasPersistedMessages
*/
public static void storeHasPersistedMessages(boolean hasPersistedMessages) {
// Store the global flag in the default preferences instead of special one
// for the GCM messages. The reason is the default preferences file is used in
// many places in Chrome and should be already cached in memory by the
// time this method is called. Therefore, it should provide a cheap way
// that (most probably) doesn't require disk access to read that global flag.
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
sharedPrefs.edit().putBoolean(HAS_PERSISTED_MESSAGES_KEY, hasPersistedMessages).apply();
}
/**
* Whether some messages are persisted and should be replayed next time
* Chrome is running. It should be cheaper to call than actually reading the
* stored messages. Call this method to decide whether there is a need to
* read any persisted messages.
* @return whether some messages are persisted.
*/
public static boolean hasPersistedMessages() {
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
return sharedPrefs.getBoolean(HAS_PERSISTED_MESSAGES_KEY, false);
}
/** /**
* Given an appId and a senderId, this methods builds a unique identifier for a subscription. * Given an appId and a senderId, this methods builds a unique identifier for a subscription.
* Currently implementation concatenates both senderId and appId. * Currently implementation concatenates both senderId and appId.
...@@ -74,12 +103,28 @@ public class LazySubscriptionsManager { ...@@ -74,12 +103,28 @@ public class LazySubscriptionsManager {
* Returns whether the subscription with the |appId| and |senderId| is lazy. * Returns whether the subscription with the |appId| and |senderId| is lazy.
*/ */
public static boolean isSubscriptionLazy(final String subscriptionId) { public static boolean isSubscriptionLazy(final String subscriptionId) {
Context context = ContextUtils.getApplicationContext(); try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
SharedPreferences sharedPrefs = Context context = ContextUtils.getApplicationContext();
context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE); SharedPreferences sharedPrefs =
Set<String> lazyIds = new HashSet<>( context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
sharedPrefs.getStringSet(FCM_LAZY_SUBSCRIPTIONS, Collections.emptySet())); Set<String> lazyIds = new HashSet<>(
return lazyIds.contains(subscriptionId); sharedPrefs.getStringSet(FCM_LAZY_SUBSCRIPTIONS, Collections.emptySet()));
return lazyIds.contains(subscriptionId);
}
}
/**
* Returns the ids of all lazy subscriptions.
* @return Set of subscriptions ids.
*/
public static Set<String> getLazySubscriptionIds() {
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
Context context = ContextUtils.getApplicationContext();
SharedPreferences sharedPrefs =
context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
return new HashSet<>(
sharedPrefs.getStringSet(FCM_LAZY_SUBSCRIPTIONS, Collections.emptySet()));
}
} }
/** /**
...@@ -124,6 +169,7 @@ public class LazySubscriptionsManager { ...@@ -124,6 +169,7 @@ public class LazySubscriptionsManager {
// Add the new message to the end. // Add the new message to the end.
queueJSON.put(message.toJSON()); queueJSON.put(message.toJSON());
sharedPrefs.edit().putString(subscriptionId, queueJSON.toString()).apply(); sharedPrefs.edit().putString(subscriptionId, queueJSON.toString()).apply();
storeHasPersistedMessages(/*hasPersistedMessages=*/true);
} catch (JSONException e) { } catch (JSONException e) {
Log.e(TAG, Log.e(TAG,
"Error when parsing the persisted message queue for subscriber:" "Error when parsing the persisted message queue for subscriber:"
...@@ -174,6 +220,17 @@ public class LazySubscriptionsManager { ...@@ -174,6 +220,17 @@ public class LazySubscriptionsManager {
return new GCMMessage[0]; return new GCMMessage[0];
} }
/**
* Deletes all persisted messages for the given subscription id.
* @param subscriptionId
*/
public static void deletePersistedMessagesForSubscriptionId(String subscriptionId) {
Context context = ContextUtils.getApplicationContext();
SharedPreferences sharedPrefs =
context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
sharedPrefs.edit().remove(subscriptionId).apply();
}
/** /**
* Filters out any messages in |messagesJSON| with the given collpase key. It returns the * Filters out any messages in |messagesJSON| with the given collpase key. It returns the
* filtered list. * filtered list.
......
...@@ -20,6 +20,8 @@ import org.chromium.base.metrics.RecordHistogram; ...@@ -20,6 +20,8 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.test.ShadowRecordHistogram; import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import java.util.Set;
/** /**
* Unit tests for LazySubscriptionsManager. * Unit tests for LazySubscriptionsManager.
*/ */
...@@ -31,6 +33,21 @@ public class LazySubscriptionsManagerTest { ...@@ -31,6 +33,21 @@ public class LazySubscriptionsManagerTest {
ShadowRecordHistogram.reset(); ShadowRecordHistogram.reset();
} }
/**
* Tests the persistence of the "hasPersistedMessages" flag.
*/
@Test
public void testHasPersistedMessages() {
// Default is false.
assertFalse(LazySubscriptionsManager.hasPersistedMessages());
LazySubscriptionsManager.storeHasPersistedMessages(true);
assertTrue(LazySubscriptionsManager.hasPersistedMessages());
LazySubscriptionsManager.storeHasPersistedMessages(false);
assertFalse(LazySubscriptionsManager.hasPersistedMessages());
}
/** /**
* Tests that lazy subscriptions are stored. * Tests that lazy subscriptions are stored.
*/ */
...@@ -60,6 +77,21 @@ public class LazySubscriptionsManagerTest { ...@@ -60,6 +77,21 @@ public class LazySubscriptionsManagerTest {
assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId)); assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
} }
@Test
public void testGetLazySubscriptionIds() {
final String subscriptionId1 = "subscription_id1";
final String subscriptionId2 = "subscription_id2";
final String subscriptionId3 = "subscription_id3";
LazySubscriptionsManager.storeLazinessInformation(subscriptionId1, true);
LazySubscriptionsManager.storeLazinessInformation(subscriptionId2, true);
LazySubscriptionsManager.storeLazinessInformation(subscriptionId3, true);
Set<String> lazySubscriptionIds = LazySubscriptionsManager.getLazySubscriptionIds();
assertEquals(3, lazySubscriptionIds.size());
assertTrue(lazySubscriptionIds.contains(subscriptionId1));
assertTrue(lazySubscriptionIds.contains(subscriptionId2));
assertTrue(lazySubscriptionIds.contains(subscriptionId3));
}
/** /**
* Tests that GCM messages are persisted and read. * Tests that GCM messages are persisted and read.
*/ */
...@@ -161,4 +193,23 @@ public class LazySubscriptionsManagerTest { ...@@ -161,4 +193,23 @@ public class LazySubscriptionsManagerTest {
RecordHistogram.getHistogramValueCountForTesting( RecordHistogram.getHistogramValueCountForTesting(
"PushMessaging.QueuedMessagesCount", 0)); "PushMessaging.QueuedMessagesCount", 0));
} }
/**
* Tests that messages with the same collapse key override each other.
*/
@Test
public void testDeletePersistedMessages() {
final String subscriptionId = "subscriptionId";
Bundle extras = new Bundle();
extras.putString("subtype", "MyAppId");
extras.putString("collapse_key", "collapseKey");
extras.putByteArray("rawData", new byte[] {});
GCMMessage message = new GCMMessage("MySenderId", extras);
LazySubscriptionsManager.persistMessage(subscriptionId, message);
assertEquals(1, LazySubscriptionsManager.readMessages(subscriptionId).length);
LazySubscriptionsManager.deletePersistedMessagesForSubscriptionId(subscriptionId);
assertEquals(0, LazySubscriptionsManager.readMessages(subscriptionId).length);
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment