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;
import com.google.android.gms.gcm.GcmListenerService;
import com.google.ipc.invalidation.ticl.android2.channel.AndroidGcmController;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
......@@ -23,6 +24,7 @@ import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.background_task_scheduler.TaskInfo;
import org.chromium.components.gcm_driver.GCMDriver;
import org.chromium.components.gcm_driver.GCMMessage;
import org.chromium.components.gcm_driver.LazySubscriptionsManager;
/**
* Receives Downstream messages and status of upstream messages from GCM.
......@@ -87,13 +89,25 @@ public class ChromeGcmListenerService extends GcmListenerService {
}
/**
* Either schedules |message| 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.
* If Chrome is backgrounded, messages coming from lazy subscriptions are
* persisted on disk and replayed next time Chrome is forgrounded. If Chrome is forgrounded or
* if the message isn't coming from a lazy subscription, this method either schedules |message|
* 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) {
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) {
Bundle extras = message.toBundle();
......
......@@ -12,6 +12,7 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.task.AsyncTask;
import java.io.IOException;
import java.util.Set;
/**
* This class is the Java counterpart to the C++ GCMDriverAndroid class.
......@@ -47,6 +48,23 @@ public class GCMDriver {
throw new IllegalStateException("Already instantiated");
}
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;
}
......
......@@ -13,6 +13,7 @@ import org.json.JSONObject;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
......@@ -29,6 +30,7 @@ import java.util.Set;
public class LazySubscriptionsManager {
private static final String TAG = "LazySubscriptions";
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 =
"org.chromium.components.gcm_driver.lazy_subscriptions";
......@@ -41,6 +43,33 @@ public class LazySubscriptionsManager {
// shouldn't be instantiated.
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.
* Currently implementation concatenates both senderId and appId.
......@@ -74,12 +103,28 @@ public class LazySubscriptionsManager {
* Returns whether the subscription with the |appId| and |senderId| is lazy.
*/
public static boolean isSubscriptionLazy(final String subscriptionId) {
Context context = ContextUtils.getApplicationContext();
SharedPreferences sharedPrefs =
context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
Set<String> lazyIds = new HashSet<>(
sharedPrefs.getStringSet(FCM_LAZY_SUBSCRIPTIONS, Collections.emptySet()));
return lazyIds.contains(subscriptionId);
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
Context context = ContextUtils.getApplicationContext();
SharedPreferences sharedPrefs =
context.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
Set<String> lazyIds = new HashSet<>(
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 {
// Add the new message to the end.
queueJSON.put(message.toJSON());
sharedPrefs.edit().putString(subscriptionId, queueJSON.toString()).apply();
storeHasPersistedMessages(/*hasPersistedMessages=*/true);
} catch (JSONException e) {
Log.e(TAG,
"Error when parsing the persisted message queue for subscriber:"
......@@ -174,6 +220,17 @@ public class LazySubscriptionsManager {
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
* filtered list.
......
......@@ -20,6 +20,8 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.util.Set;
/**
* Unit tests for LazySubscriptionsManager.
*/
......@@ -31,6 +33,21 @@ public class LazySubscriptionsManagerTest {
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.
*/
......@@ -60,6 +77,21 @@ public class LazySubscriptionsManagerTest {
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.
*/
......@@ -161,4 +193,23 @@ public class LazySubscriptionsManagerTest {
RecordHistogram.getHistogramValueCountForTesting(
"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