chromium/chrome/android/java/src/org/chromium/chrome/browser/partnerbookmarks/PartnerBookmarksFaviconThrottle.java

// Copyright 2017 The Chromium Authors
// 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.partnerbookmarks;

import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.text.format.DateUtils;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * Stores failed favicon loads along with a timestamp determining what point we can attempt
 * retrieval again, as a way of throttling the number of requests made for previously failed favicon
 * fetch attempts.
 */
public class PartnerBookmarksFaviconThrottle {
    private static final String PREFERENCES_NAME = "partner_bookmarks_favicon_throttle";
    private static final long FAVICON_RETRIEVAL_TIMEOUT_MS = DateUtils.DAY_IN_MILLIS * 30;

    private final SharedPreferences mSharedPreferences;

    private Map<String, Long> mCurrentEntries;
    private Map<String, Long> mNewEntries;

    public PartnerBookmarksFaviconThrottle() {
        this(PREFERENCES_NAME);
    }

    @VisibleForTesting
    PartnerBookmarksFaviconThrottle(String name) {
        mSharedPreferences = ContextUtils.getApplicationContext().getSharedPreferences(name, 0);
        init();
    }

    /**
     * Reads the favicon retrieval timestamp information from our own {@link SharedPreferences}.
     *
     * Suppressing "unchecked" because we're 100% sure we're storing only <String, Long> pairs.
     */
    @SuppressWarnings("unchecked")
    @VisibleForTesting
    void init() {
        mCurrentEntries = (Map<String, Long>) mSharedPreferences.getAll();
        mNewEntries = new HashMap<>();
    }

    /**
     * Writes the new map that was built as a result of the calls to {@link #onFaviconFetched} to
     * disk in our {@link SharedPreferences}. This overwrites previously stored entries.
     */
    public void commit() {
        assert mNewEntries != null;

        // Save ourselves a write to disk if the current entries and new entries are identical.
        if (mCurrentEntries.equals(mNewEntries)) {
            return;
        }

        Editor editor = mSharedPreferences.edit();
        editor.clear();
        for (Map.Entry<String, Long> entry : mNewEntries.entrySet()) {
            editor.putLong(entry.getKey(), entry.getValue());
        }
        editor.apply();
    }

    /**
     * Calling this with each favicon fetch URL and result builds the new output entries to be
     * written to disk when {@link #commit} is called.
     *
     * @param url The page URL we attempted to fetch a favicon for.
     * @param result The {@link FaviconFetchResult} response we got for this URL.
     */
    public void onFaviconFetched(String url, @FaviconFetchResult int result) {
        assert mCurrentEntries != null;
        assert mNewEntries != null;

        if (result == FaviconFetchResult.FAILURE_SERVER_ERROR) {
            mNewEntries.put(url, System.currentTimeMillis() + FAVICON_RETRIEVAL_TIMEOUT_MS);
        } else if (!isSuccessfulFetchResult(result)
                && !shouldFetchFromServerIfNecessary(url)
                && (System.currentTimeMillis() < mCurrentEntries.get(url))) {
            // Keep storing an entry if it hasn't yet expired and we get didn't just get a success
            // response.
            mNewEntries.put(url, mCurrentEntries.get(url));
        }
    }

    private boolean isSuccessfulFetchResult(@FaviconFetchResult int result) {
        return result == FaviconFetchResult.SUCCESS_FROM_CACHE
                || result == FaviconFetchResult.SUCCESS_FROM_SERVER;
    }

    /**
     * Determines, based on the contents of our entry set, whether or not we should even attempt to
     * reach out to a server to retrieve a favicon that isn't currently in our favicon image cache.
     *
     * @param url The page URL we need a favicon for.
     * @return Whether or not we should fetch the favicon from server if necessary.
     */
    public boolean shouldFetchFromServerIfNecessary(String url) {
        Long expiryTimeMs = getExpiryOf(url);
        return expiryTimeMs == null || System.currentTimeMillis() >= expiryTimeMs;
    }

    /**
     * Gets the expiry time in ms of a particular URL for which we're fetching a favicon. URLs that
     * have previously failed to retrieve a favicon from a server will have a value at which point
     * we should attempt a retrieval again, otherwise we return null for entries not in our set of
     * entries.
     *
     * @param url The page URL we're trying to fetch a favicon for.
     * @return The expiry time of the favicon fetching restriction in milliseconds, if we have a
     *         corresponding entry for this URL.
     */
    private Long getExpiryOf(String url) {
        assert mCurrentEntries != null;

        if (mCurrentEntries.containsKey(url)) {
            return mCurrentEntries.get(url);
        }
        return null;
    }

    /**
     * Called after tests so we don't leave behind test {@link SharedPreferences}, and have data
     * from one test run into another.
     */
    @VisibleForTesting
    void clearEntries() {
        mSharedPreferences.edit().clear().apply();
    }

    /**
     * @return Number of stored entries.
     */
    @VisibleForTesting
    int numEntries() {
        assert mCurrentEntries != null;
        return mCurrentEntries.size();
    }
}