// 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.feedback; import android.app.Activity; import android.graphics.Bitmap; import android.os.Bundle; import android.os.SystemClock; import android.text.TextUtils; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.StatisticsRecorderAndroid; import org.chromium.blimp_public.BlimpClientContext; import org.chromium.chrome.browser.blimp.BlimpClientContextFactory; import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.components.variations.VariationsAssociatedData; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** * A class which collects generic information about Chrome which is useful for all types of * feedback, and provides it as a {@link Bundle}. * * Creating a {@link FeedbackCollector} initiates asynchronous operations for gathering feedback * data, which may or not finish before the bundle is requested by calling {@link #getBundle()}. * * Interacting with the {@link FeedbackCollector} is only allowed on the main thread. */ public class FeedbackCollector implements ConnectivityTask.ConnectivityResult, ScreenshotTask.ScreenshotTaskCallback { /** * A user visible string describing the current URL. */ @VisibleForTesting static final String URL_KEY = "URL"; /** * The timeout (ms) for gathering data asynchronously. * This timeout is ignored for taking screenshots. */ private static final int TIMEOUT_MS = 500; /** * The timeout (ms) for gathering connection data. * This may be more than the main timeout as taking the screenshot can take more time than * {@link #TIMEOUT_MS}. */ private static final int CONNECTIVITY_CHECK_TIMEOUT_MS = 5000; private final Map<String, String> mData; private final Profile mProfile; private final String mUrl; private final FeedbackResult mCallback; private final long mCollectionStartTime; // Not final because created during init. Should be used as a final member. protected ConnectivityTask mConnectivityTask; /** * An optional description for the feedback report. */ private String mDescription; /** * An optional screenshot for the feedback report. */ private Bitmap mScreenshot; /** * All the registered histograms as JSON text. */ private String mHistograms; /** * A flag indicating whether gathering connection data has finished. */ private boolean mConnectivityTaskFinished; /** * A flag indicating whether taking a screenshot has finished. */ private boolean mScreenshotTaskFinished; /** * A flag indicating whether the result has already been posted. This is used to ensure that * the result is not posted again if a timeout happens. */ private boolean mResultPosted; /** * A callback for when the gathering of feedback data has finished. This may be called either * when all data has been collected, or after a timeout. */ public interface FeedbackResult { /** * Called when feedback data collection result is ready. * @param collector the {@link FeedbackCollector} to retrieve the data from. */ void onResult(FeedbackCollector collector); } /** * Creates a {@link FeedbackCollector} and starts asynchronous operations to gather extra data. * @param profile the current Profile. * @param url The URL of the current tab to include in the feedback the user sends, if any. * This parameter may be null. * @param callback The callback which is invoked when feedback gathering is finished. * @return the created {@link FeedbackCollector}. */ public static FeedbackCollector create( Activity activity, Profile profile, @Nullable String url, FeedbackResult callback) { ThreadUtils.assertOnUiThread(); return new FeedbackCollector(activity, profile, url, callback); } @VisibleForTesting FeedbackCollector(Activity activity, Profile profile, String url, FeedbackResult callback) { mData = new HashMap<>(); mProfile = profile; mUrl = url; mCallback = callback; mCollectionStartTime = SystemClock.elapsedRealtime(); init(activity); } @VisibleForTesting void init(Activity activity) { postTimeoutTask(); mConnectivityTask = ConnectivityTask.create(mProfile, CONNECTIVITY_CHECK_TIMEOUT_MS, this); ScreenshotTask.create(activity, this); if (!mProfile.isOffTheRecord()) { mHistograms = StatisticsRecorderAndroid.toJson(); } } /** * {@link ConnectivityTask.ConnectivityResult} implementation. */ @Override public void onResult(ConnectivityTask.FeedbackData feedbackData) { ThreadUtils.assertOnUiThread(); mConnectivityTaskFinished = true; Map<String, String> connectivityMap = feedbackData.toMap(); mData.putAll(connectivityMap); maybePostResult(); } /** * {@link ScreenshotTask.ScreenshotTaskCallback} implementation. */ @Override public void onGotBitmap(@Nullable Bitmap bitmap) { ThreadUtils.assertOnUiThread(); mScreenshotTaskFinished = true; mScreenshot = bitmap; maybePostResult(); } private void postTimeoutTask() { ThreadUtils.postOnUiThreadDelayed(new Runnable() { @Override public void run() { maybePostResult(); } }, TIMEOUT_MS); } @VisibleForTesting void maybePostResult() { ThreadUtils.assertOnUiThread(); if (mCallback == null) return; if (mResultPosted) return; // Always wait for screenshot. if (!mScreenshotTaskFinished) return; if (!mConnectivityTaskFinished && !hasTimedOut()) return; mResultPosted = true; ThreadUtils.postOnUiThread(new Runnable() { @Override public void run() { mCallback.onResult(FeedbackCollector.this); } }); } @VisibleForTesting boolean hasTimedOut() { return SystemClock.elapsedRealtime() - mCollectionStartTime > TIMEOUT_MS; } /** * Adds a key-value pair of data to be included in the feedback report. This data * is user visible and should only contain single-line forms of data, not long Strings. * @param key the user visible key. * @param value the user visible value. */ public void add(String key, String value) { ThreadUtils.assertOnUiThread(); mData.put(key, value); } /** * Sets the default description to invoke feedback with. * @param description the user visible description. */ public void setDescription(String description) { ThreadUtils.assertOnUiThread(); mDescription = description; } /** * @return the default description to invoke feedback with. */ @VisibleForTesting public String getDescription() { ThreadUtils.assertOnUiThread(); return mDescription; } /** * Sets the screenshot to use for the feedback report. * @param screenshot the user visible screenshot. */ @VisibleForTesting public void setScreenshot(Bitmap screenshot) { ThreadUtils.assertOnUiThread(); mScreenshot = screenshot; } /** * @return the screenshot to use for the feedback report. */ @VisibleForTesting public Bitmap getScreenshot() { ThreadUtils.assertOnUiThread(); return mScreenshot; } /** * @return All the registered histograms as JSON text. */ public String getHistograms() { return mHistograms; } /** * @return the collected data as a {@link Bundle}. */ @VisibleForTesting public Bundle getBundle() { ThreadUtils.assertOnUiThread(); addUrl(); addConnectivityData(); addDataReductionProxyData(); addVariationsData(); addBlimpData(); return asBundle(); } private void addUrl() { if (!TextUtils.isEmpty(mUrl)) { mData.put(URL_KEY, mUrl); } } private void addConnectivityData() { if (mConnectivityTaskFinished) return; Map<String, String> connectivityMap = mConnectivityTask.get().toMap(); mData.putAll(connectivityMap); } private void addDataReductionProxyData() { if (mProfile.isOffTheRecord()) return; Map<String, String> dataReductionProxyMap = DataReductionProxySettings.getInstance().toFeedbackMap(); mData.putAll(dataReductionProxyMap); } private void addVariationsData() { if (mProfile.isOffTheRecord()) return; mData.putAll(VariationsAssociatedData.getFeedbackMap()); } private void addBlimpData() { if (mProfile.isOffTheRecord()) return; BlimpClientContext blimpClientContext = BlimpClientContextFactory.getBlimpClientContextForProfile(mProfile); mData.putAll(blimpClientContext.getFeedbackMap()); } private Bundle asBundle() { Bundle bundle = new Bundle(); for (Map.Entry<String, String> entry : mData.entrySet()) { bundle.putString(entry.getKey(), entry.getValue()); } return bundle; } }