// 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.gsa;
import android.text.TextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchObserver;
import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.components.sync.ModelType;
import org.chromium.components.sync.PassphraseType;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
/**
* Reports context to GSA for search quality.
*/
public class ContextReporter {
private static final String TAG = "GSA";
// Values for UMA histogram.
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_GSA_NOT_AVAILABLE = 1;
public static final int STATUS_SYNC_NOT_INITIALIZED = 2;
public static final int STATUS_SYNC_NOT_SYNCING_URLS = 3;
public static final int STATUS_SYNC_NOT_KEYSTORE_PASSPHRASE = 4;
public static final int STATUS_SYNC_OTHER = 5;
public static final int STATUS_SVELTE_DEVICE = 6;
public static final int STATUS_NO_TAB = 7;
public static final int STATUS_INCOGNITO = 8;
public static final int STATUS_INVALID_SCHEME = 9;
public static final int STATUS_TAB_ID_MISMATCH = 10;
public static final int STATUS_DUP_TITLE_CHANGE = 11;
public static final int STATUS_CONNECTION_FAILED = 12;
public static final int STATUS_SYNC_NOT_READY_AT_REPORT_TIME = 13;
public static final int STATUS_NOT_SIGNED_IN = 14;
public static final int STATUS_GSA_ACCOUNT_MISSING = 15;
public static final int STATUS_GSA_ACCOUNT_MISMATCH = 16;
public static final int STATUS_RESULT_IS_NULL = 17;
public static final int STATUS_RESULT_FAILED = 18;
public static final int STATUS_SUCCESS_WITH_SELECTION = 19;
// This should always stay last and have the highest number.
private static final int STATUS_BOUNDARY = 20;
private final ChromeActivity mActivity;
private final GSAContextReportDelegate mDelegate;
private TabModelSelectorTabObserver mSelectorTabObserver;
private TabModelObserver mModelObserver;
private ContextualSearchObserver mContextualSearchObserver;
private boolean mLastContextWasTitleChange;
private final AtomicBoolean mContextInUse;
/**
* Creates a ContextReporter for an Activity.
* @param activity Chrome Activity which context will be reported.
* @param controller used to communicate with GSA
*/
public ContextReporter(ChromeActivity activity, GSAContextReportDelegate controller) {
mActivity = activity;
mDelegate = controller;
mContextInUse = new AtomicBoolean(false);
Log.d(TAG, "Created a new ContextReporter");
}
/**
* Starts reporting context.
*/
public void enable() {
Tab currentTab = mActivity.getActivityTab();
reportUsageOfCurrentContextIfPossible(currentTab, false, null);
TabModelSelector selector = mActivity.getTabModelSelector();
assert selector != null;
if (mSelectorTabObserver == null) {
mSelectorTabObserver = new TabModelSelectorTabObserver(selector) {
@Override
public void onTitleUpdated(Tab tab) {
// Report usage declaring this as a title change.
reportUsageOfCurrentContextIfPossible(tab, true, null);
}
@Override
public void onUrlUpdated(Tab tab) {
reportUsageOfCurrentContextIfPossible(tab, false, null);
}
};
}
if (mModelObserver == null) {
assert !selector.getModels().isEmpty();
mModelObserver = new EmptyTabModelObserver() {
@Override
public void didSelectTab(Tab tab, TabSelectionType type, int lastId) {
reportUsageOfCurrentContextIfPossible(tab, false, null);
}
};
for (TabModel model : selector.getModels()) {
model.addObserver(mModelObserver);
}
}
if (mContextualSearchObserver == null && mActivity.getContextualSearchManager() != null) {
mContextualSearchObserver = new ContextualSearchObserver() {
@Override
public void onShowContextualSearch(GSAContextDisplaySelection contextSelection) {
if (contextSelection != null) reportDisplaySelection(contextSelection);
}
@Override
public void onHideContextualSearch() {
reportDisplaySelection(null);
}
};
mActivity.getContextualSearchManager().addObserver(mContextualSearchObserver);
}
}
/**
* Stops reporting context. Called when the app goes to the background.
*/
public void disable() {
reportUsageEndedIfNecessary();
if (mSelectorTabObserver != null) {
mSelectorTabObserver.destroy();
mSelectorTabObserver = null;
}
if (mModelObserver != null) {
for (TabModel model : mActivity.getTabModelSelector().getModels()) {
model.removeObserver(mModelObserver);
}
mModelObserver = null;
}
if (mContextualSearchObserver != null && mActivity.getContextualSearchManager() != null) {
mActivity.getContextualSearchManager().removeObserver(mContextualSearchObserver);
mContextualSearchObserver = null;
}
}
/**
* Reports that the given display selection has been established for the current tab.
* @param displaySelection The information about the selection being displayed.
*/
private void reportDisplaySelection(@Nullable GSAContextDisplaySelection displaySelection) {
Tab currentTab = mActivity.getActivityTab();
reportUsageOfCurrentContextIfPossible(currentTab, false, displaySelection);
}
private void reportUsageEndedIfNecessary() {
if (mContextInUse.compareAndSet(true, false)) mDelegate.reportContextUsageEnded();
}
private void reportUsageOfCurrentContextIfPossible(
Tab tab, boolean isTitleChange, @Nullable GSAContextDisplaySelection displaySelection) {
Tab currentTab = mActivity.getActivityTab();
if (currentTab == null || currentTab.isIncognito()) {
if (currentTab == null) {
reportStatus(STATUS_NO_TAB);
Log.d(TAG, "Not reporting, tab is null");
} else {
reportStatus(STATUS_INCOGNITO);
Log.d(TAG, "Not reporting, tab is incognito");
}
reportUsageEndedIfNecessary();
return;
}
String currentUrl = currentTab.getUrl();
if (TextUtils.isEmpty(currentUrl) || !(currentUrl.startsWith(UrlConstants.HTTP_SCHEME)
|| currentUrl.startsWith(UrlConstants.HTTPS_SCHEME))) {
reportStatus(STATUS_INVALID_SCHEME);
Log.d(TAG, "Not reporting, URL scheme is invalid");
reportUsageEndedIfNecessary();
return;
}
// Check whether this is a context change we would like to report.
if (currentTab.getId() != tab.getId()) {
reportStatus(STATUS_TAB_ID_MISMATCH);
Log.d(TAG, "Not reporting, tab ID doesn't match");
return;
}
if (isTitleChange && mLastContextWasTitleChange) {
reportStatus(STATUS_DUP_TITLE_CHANGE);
Log.d(TAG, "Not reporting, repeated title update");
return;
}
reportUsageEndedIfNecessary();
mDelegate.reportContext(currentTab.getUrl(), currentTab.getTitle(), displaySelection);
mLastContextWasTitleChange = isTitleChange;
mContextInUse.set(true);
}
/**
* Records the given status via UMA.
* Use one of the STATUS_* constants above.
*/
public static void reportStatus(int status) {
RecordHistogram.recordEnumeratedHistogram(
"Search.IcingContextReportingStatus", status, STATUS_BOUNDARY);
}
/**
* Records an appropriate status via UMA given the current sync status.
*/
public static void reportSyncStatus(@Nullable ProfileSyncService syncService) {
if (syncService == null || !syncService.isBackendInitialized()) {
reportStatus(STATUS_SYNC_NOT_INITIALIZED);
} else if (!syncService.getActiveDataTypes().contains(ModelType.TYPED_URLS)) {
reportStatus(STATUS_SYNC_NOT_SYNCING_URLS);
} else if (!syncService.getPassphraseType().equals(PassphraseType.KEYSTORE_PASSPHRASE)) {
reportStatus(STATUS_SYNC_NOT_KEYSTORE_PASSPHRASE);
} else {
reportStatus(STATUS_SYNC_OTHER);
}
}
}