blob: cdd2c1a4096ae62d61e09bab3b744705f823418f [file] [log] [blame]
// Copyright 2017 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.feedback;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.util.Pair;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.util.ArrayList;
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.
*/
public class FeedbackCollector implements Runnable {
/** The timeout for gathering data asynchronously. This timeout is ignored for screenshots. */
private static final int TIMEOUT_MS = 500;
private final List<FeedbackSource> mSynchronousSources;
private final List<AsyncFeedbackSource> mAsynchronousSources;
private final long mStartTime = SystemClock.elapsedRealtime();
private final String mCategoryTag;
private final String mDescription;
private ScreenshotSource mScreenshotTask;
/** The callback is cleared once notified so we will never notify the caller twice. */
private Callback<FeedbackCollector> mCallback;
public FeedbackCollector(Activity activity, Profile profile, @Nullable String url,
@Nullable String categoryTag, @Nullable String description,
@Nullable String feedbackContext, boolean takeScreenshot,
Callback<FeedbackCollector> callback) {
mCategoryTag = categoryTag;
mDescription = description;
mCallback = callback;
// 1. Build all synchronous and asynchronous sources.
mSynchronousSources = buildSynchronousFeedbackSources(profile, url, feedbackContext);
mAsynchronousSources = buildAsynchronousFeedbackSources(profile);
// 2. Build the screenshot task if necessary.
if (takeScreenshot) mScreenshotTask = buildScreenshotSource(activity);
// 3. Start all asynchronous sources and the screenshot task.
CollectionUtil.forEach(mAsynchronousSources, source -> 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. Sanity check in case everything finished or we have no sources.
checkIfReady();
}
@VisibleForTesting
protected List<FeedbackSource> buildSynchronousFeedbackSources(
Profile profile, @Nullable String url, @Nullable String feedbackContext) {
List<FeedbackSource> sources = new ArrayList<>();
// This is the list of all synchronous sources of feedback. Please add new synchronous
// entries here.
sources.addAll(AppHooks.get().getAdditionalFeedbackSources().getSynchronousSources());
sources.add(new UrlFeedbackSource(url));
sources.add(new VariationsFeedbackSource(profile));
sources.add(new DataReductionProxyFeedbackSource(profile));
sources.add(new HistogramFeedbackSource(profile));
sources.add(new LowEndDeviceFeedbackSource());
sources.add(new IMEFeedbackSource());
sources.add(new PermissionFeedbackSource());
sources.add(new FeedbackContextFeedbackSource(feedbackContext));
sources.add(new DuetFeedbackSource());
sources.add(new InterestFeedFeedbackSource());
// Sanity check in case a source is added to the wrong list.
for (FeedbackSource source : sources) {
assert !(source instanceof AsyncFeedbackSource);
}
return sources;
}
@VisibleForTesting
protected List<AsyncFeedbackSource> buildAsynchronousFeedbackSources(Profile profile) {
List<AsyncFeedbackSource> sources = new ArrayList<>();
// This is the list of all asynchronous sources of feedback. Please add new asynchronous
// entries here.
sources.addAll(AppHooks.get().getAdditionalFeedbackSources().getAsynchronousSources());
sources.add(new ConnectivityFeedbackSource(profile));
sources.add(new SystemInfoFeedbackSource());
sources.add(new ProcessIdFeedbackSource());
return sources;
}
@VisibleForTesting
protected ScreenshotSource buildScreenshotSource(Activity activity) {
return new ScreenshotTask(activity);
}
/** @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;
}
/**
* 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();
doWorkOnAllFeedbackSources(source -> {
Map<String, String> feedback = source.getFeedback();
if (feedback == null) return;
CollectionUtil.forEach(feedback, e -> { 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<>();
doWorkOnAllFeedbackSources(source -> {
Pair<String, String> log = source.getLogs();
if (log == null) return;
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;
}
}
final Callback<FeedbackCollector> callback = mCallback;
mCallback = null;
PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() {
@Override
public void run() {
callback.onResult(FeedbackCollector.this);
}
});
}
private void doWorkOnAllFeedbackSources(Callback<FeedbackSource> worker) {
CollectionUtil.forEach(mSynchronousSources, worker);
CollectionUtil.forEach(mAsynchronousSources, worker);
}
}