blob: 546c582d87407c7978c47802c3adca6f7b435f40 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.invalidation;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.util.ObjectsCompat;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.AsyncTask;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.components.invalidation.PendingInvalidation;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.sync.AndroidSyncSettings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* A class for controlling whether an invalidation should be notified immediately, or should be
* delayed until Chrome comes to the foreground again.
*/
public class DelayedInvalidationsController {
private static final String TAG = "invalidation";
private static final String DELAYED_ACCOUNT_NAME = "delayed_account";
private static final String DELAYED_INVALIDATIONS = "delayed_invalidations";
private static class LazyHolder {
private static final DelayedInvalidationsController INSTANCE =
new DelayedInvalidationsController();
}
public static DelayedInvalidationsController getInstance() {
return LazyHolder.INSTANCE;
}
@VisibleForTesting
DelayedInvalidationsController() {}
/**
* Notify any invalidations that were delayed while Chromium was backgrounded.
* @return whether there were any invalidations pending to be notified.
*/
public boolean notifyPendingInvalidations() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
String accountName = prefs.getString(DELAYED_ACCOUNT_NAME, null);
if (accountName == null) {
Log.d(TAG, "No pending invalidations.");
return false;
} else {
Log.d(TAG, "Handling pending invalidations.");
Account account = AccountManagerFacade.createAccountFromName(accountName);
List<Bundle> bundles = popPendingInvalidations();
notifyInvalidationsOnBackgroundThread(account, bundles);
return true;
}
}
/**
* Calls ContentResolver.requestSync() in a separate thread as it performs some blocking
* IO operations.
*/
@VisibleForTesting
void notifyInvalidationsOnBackgroundThread(final Account account, final List<Bundle> bundles) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... unused) {
String contractAuthority = AndroidSyncSettings.getContractAuthority();
for (Bundle bundle : bundles) {
ContentResolver.requestSync(account, contractAuthority, bundle);
}
return null;
}
}.execute();
}
/**
* Stores preferences to indicate that an invalidation has arrived, but dropped on the floor.
*/
@VisibleForTesting
void addPendingInvalidation(String account, PendingInvalidation invalidation) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
String oldAccount = prefs.getString(DELAYED_ACCOUNT_NAME, null);
// Make sure to construct a new set so it can be modified safely. See crbug.com/568369.
Set<String> invals = new HashSet<String>(
prefs.getStringSet(DELAYED_INVALIDATIONS, Collections.<String>emptySet()));
assert invals.isEmpty() || oldAccount != null;
boolean invalidateAllTypes = false;
// We invalidate all types if:
// - the account has changed
// - we were in "invalidate all types" mode already
// - new invalidation indicates to invalidate all types by setting source to 0
// - adding invalidation to the current set failed
if (oldAccount != null && !oldAccount.equals(account)) invalidateAllTypes = true;
if (oldAccount != null && invals.isEmpty()) invalidateAllTypes = true;
if (invalidation.mObjectSource == 0) invalidateAllTypes = true;
if (!invalidateAllTypes && !addInvalidationToSet(invalidation, invals)) {
invalidateAllTypes = true;
}
SharedPreferences.Editor editor = prefs.edit();
editor.putString(DELAYED_ACCOUNT_NAME, account);
if (invalidateAllTypes) {
editor.remove(DELAYED_INVALIDATIONS);
} else {
editor.putStringSet(DELAYED_INVALIDATIONS, invals);
}
editor.apply();
}
/**
* Adds newInvalidation into set of encoded invalidations. Invalidations with the same id/source
* and lower version are removed from the set. If invalidation with same or higher version is
* is present, then new invalidation is discarded.
* @return true if update is successful, false when decoding invalidation from string fails.
*/
private boolean addInvalidationToSet(
PendingInvalidation newInvalidation, Set<String> invalidations) {
for (Iterator<String> iter = invalidations.iterator(); iter.hasNext();) {
String encodedInvalidation = iter.next();
PendingInvalidation invalidation =
PendingInvalidation.decodeToPendingInvalidation(encodedInvalidation);
if (invalidation == null) return false;
if (ObjectsCompat.equals(invalidation.mObjectId, newInvalidation.mObjectId)
&& invalidation.mObjectSource == newInvalidation.mObjectSource) {
if (invalidation.mVersion >= newInvalidation.mVersion) return true;
iter.remove();
}
}
invalidations.add(newInvalidation.encodeToString());
return true;
}
private List<Bundle> popPendingInvalidations() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
assert prefs.contains(DELAYED_ACCOUNT_NAME);
Set<String> savedInvalidations = prefs.getStringSet(DELAYED_INVALIDATIONS, null);
clearPendingInvalidations();
// Absence of specific invalidations indicates invalidate all types.
if (savedInvalidations == null) return Arrays.asList(new Bundle());
List<Bundle> bundles = new ArrayList<Bundle>(savedInvalidations.size());
for (String invalidation : savedInvalidations) {
Bundle bundle = PendingInvalidation.decodeToBundle(invalidation);
if (bundle == null) {
Log.e(TAG, "Error parsing saved invalidation. Invalidating all.");
return Arrays.asList(new Bundle());
}
bundles.add(bundle);
}
return bundles;
}
/**
* If there are any pending invalidations, they will be cleared.
*/
@VisibleForTesting
public void clearPendingInvalidations() {
SharedPreferences.Editor editor =
ContextUtils.getAppSharedPreferences().edit();
editor.putString(DELAYED_ACCOUNT_NAME, null);
editor.putStringSet(DELAYED_INVALIDATIONS, null);
editor.apply();
}
@VisibleForTesting
boolean shouldNotifyInvalidation(Bundle extras) {
return isManualRequest(extras) || ApplicationStatus.hasVisibleActivities();
}
private static boolean isManualRequest(Bundle extras) {
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) {
Log.d(TAG, "Manual sync requested.");
return true;
}
return false;
}
}