// 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.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Bundle; import org.chromium.base.ApplicationStatus; 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.AccountManagerHelper; import org.chromium.components.sync.AndroidSyncSettings; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; 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(final Context context) { 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 = AccountManagerHelper.createAccountFromName(accountName); List<Bundle> bundles = popPendingInvalidations(context); notifyInvalidationsOnBackgroundThread(context, account, bundles); return true; } } /** * Calls ContentResolver.requestSync() in a separate thread as it performs some blocking * IO operations. */ @VisibleForTesting void notifyInvalidationsOnBackgroundThread( final Context context, final Account account, final List<Bundle> bundles) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... unused) { String contractAuthority = AndroidSyncSettings.getContractAuthority(context); 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(Context context, 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, new HashSet<String>(1))); assert invals.isEmpty() || oldAccount != null; if (oldAccount != null && !oldAccount.equals(account)) { invals.clear(); } SharedPreferences.Editor editor = prefs.edit(); editor.putString(DELAYED_ACCOUNT_NAME, account); if (invalidation.mObjectSource == 0 || (oldAccount != null && invals.isEmpty())) { editor.putStringSet(DELAYED_INVALIDATIONS, null); } else { invals.add(invalidation.encodeToString()); editor.putStringSet(DELAYED_INVALIDATIONS, invals); } editor.apply(); } private List<Bundle> popPendingInvalidations(final Context context) { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); assert prefs.contains(DELAYED_ACCOUNT_NAME); Set<String> savedInvalidations = prefs.getStringSet(DELAYED_INVALIDATIONS, null); clearPendingInvalidations(context); // 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(Context context) { 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; } }