chromium/chrome/browser/feedback/android/java/src/org/chromium/chrome/browser/feedback/FeedbackCollector.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.feedback;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.common.collect.Iterables;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;

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

/**
 * Used for gathering a variety of feedback from various components in Chrome and bundling it into
 * a set of Key - Value pairs used to submit feedback requests.
 * @param <T> Initialization params used by subclasses for the feedback source builders.
 */
public abstract class FeedbackCollector<T> implements Runnable {
    /** The timeout for gathering data asynchronously. This timeout is ignored for screenshots. */
    private static final int TIMEOUT_MS = 500;

    private final long mStartTime = SystemClock.elapsedRealtime();

    private final String mCategoryTag;
    private final String mDescription;
    private String mAccountInUse;

    private List<FeedbackSource> mSynchronousSources;
    @VisibleForTesting protected List<AsyncFeedbackSource> mAsynchronousSources;

    private ScreenshotSource mScreenshotTask;

    /** The callback is cleared once notified so we will never notify the caller twice. */
    private Callback<FeedbackCollector> mCallback;

    public FeedbackCollector(
            @Nullable String categoryTag,
            @Nullable String description,
            Callback<FeedbackCollector> callback) {
        mCategoryTag = categoryTag;
        mDescription = description;
        mCallback = callback;
    }

    // Subclasses must invoke init() at construction time.
    protected void init(
            Activity activity,
            @Nullable ScreenshotSource screenshotTask,
            T initParams,
            Profile profile) {
        // 1. Build all synchronous and asynchronous sources and determine the currently signed in
        //    account.
        mSynchronousSources = buildSynchronousFeedbackSources(activity, initParams);
        mAsynchronousSources = buildAsynchronousFeedbackSources(initParams);
        IdentityManager identityManager =
                IdentityServicesProvider.get().getIdentityManager(profile);
        if (identityManager != null) {
            mAccountInUse =
                    CoreAccountInfo.getEmailFrom(
                            identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN));
        }

        // Validation check in case a source is added to the wrong list.
        for (FeedbackSource source : mSynchronousSources) {
            assert !(source instanceof AsyncFeedbackSource);
        }

        // 2. Set |mScreenshotTask| if not null.
        if (screenshotTask != null) mScreenshotTask = screenshotTask;

        // 3. Start all asynchronous sources and the screenshot task.
        for (var source : mAsynchronousSources) {
            source.start(this);
        }
        if (mScreenshotTask != null) mScreenshotTask.capture(this);

        // 4. Kick off a task to timeout the async sources.
        ThreadUtils.postOnUiThreadDelayed(this, TIMEOUT_MS);

        // 5. Validation check in case everything finished or we have no sources.
        checkIfReady();
    }

    @VisibleForTesting
    @NonNull
    protected abstract List<FeedbackSource> buildSynchronousFeedbackSources(
            Activity activity, T initParams);

    @VisibleForTesting
    @NonNull
    protected abstract List<AsyncFeedbackSource> buildAsynchronousFeedbackSources(T initParams);

    /** @return The category tag for this feedback report. */
    public String getCategoryTag() {
        return mCategoryTag;
    }

    /** @return The description of this feedback report. */
    public String getDescription() {
        return mDescription;
    }

    /** @return The currently signed in account, or null if the user is not signed in. */
    public @Nullable String getAccountInUse() {
        return mAccountInUse;
    }

    /**
     * Deprecated.  Please use {@link #getLogs()} instead for all potentially large feedback data.
     * @return Returns the histogram data from {@link #getLogs()}.
     */
    @Deprecated
    public String getHistograms() {
        return getLogs().get(HistogramFeedbackSource.HISTOGRAMS_KEY);
    }

    /**
     * After calling this, this collector will not notify the {@link Callback} specified in the
     * constructor (if it hasn't already).
     *
     * @return A {@link Bundle} containing all of the feedback for this report.
     * @see #getLogs() to get larger feedback data (logs).
     */
    public Bundle getBundle() {
        ThreadUtils.assertOnUiThread();

        // At this point we will no longer update the caller if we get more info from sources.
        mCallback = null;

        Bundle bundle = new Bundle();
        for (var source : getAllSources()) {
            Map<String, String> feedback = source.getFeedback();
            if (feedback == null) continue;

            for (var e : feedback.entrySet()) {
                bundle.putString(e.getKey(), e.getValue());
            }
        }
        return bundle;
    }

    /**
     * After calling this, this collector will not notify the {@link Callback} specified in the
     * constructor (if it hasn't already).
     *
     * @return A {@link Map} containing all of the logs for this report.
     * @see #getBundle() to get smaller feedback data (key -> value).
     */
    public Map<String, String> getLogs() {
        ThreadUtils.assertOnUiThread();

        // At this point we will no longer update the caller if we get more info from sources.
        mCallback = null;

        Map<String, String> logs = new HashMap<>();
        for (var source : getAllSources()) {
            Pair<String, String> log = source.getLogs();
            if (log == null) continue;

            logs.put(log.first, log.second);
        }
        return logs;
    }

    /** @return A screenshot for this report (if one was able to be taken). */
    public @Nullable Bitmap getScreenshot() {
        return mScreenshotTask == null ? null : mScreenshotTask.getScreenshot();
    }

    /**
     * Allows overriding the internal screenshot logic to always return {@code screenshot}.
     * @param screenshot The screenshot {@link Bitmap} to use.
     */
    public void setScreenshot(@Nullable Bitmap screenshot) {
        mScreenshotTask = new StaticScreenshotSource(screenshot);
        mScreenshotTask.capture(this);
    }

    /* Called whenever an AsyncFeedbackCollector is done querying data or we have timed out. */
    @Override
    public void run() {
        checkIfReady();
    }

    private void checkIfReady() {
        if (mCallback == null) return;

        // The screenshot capture overrides the timeout.
        if (mScreenshotTask != null && !mScreenshotTask.isReady()) return;

        if (mAsynchronousSources.size() > 0
                && SystemClock.elapsedRealtime() - mStartTime < TIMEOUT_MS) {
            for (AsyncFeedbackSource source : mAsynchronousSources) {
                if (!source.isReady()) return;
            }
        }

        RecordHistogram.recordMediumTimesHistogram(
                "Feedback.Duration.FetchSystemInformation",
                SystemClock.elapsedRealtime() - mStartTime);
        final Callback<FeedbackCollector> callback = mCallback;
        mCallback = null;

        PostTask.postTask(TaskTraits.UI_DEFAULT, callback.bind(this));
    }

    private Iterable<FeedbackSource> getAllSources() {
        return Iterables.concat(mSynchronousSources, mAsynchronousSources);
    }
}