// 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.compositor.bottombar.contextualsearch;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchBlacklist.BlacklistReason;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchHeuristics;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchUma;
import java.util.Locale;
/**
* This class is responsible for all the logging related to Contextual Search.
*/
public class ContextualSearchPanelMetrics {
private static final int MILLISECONDS_TO_NANOSECONDS = 1000000;
// Flags for logging.
private BlacklistReason mBlacklistReason;
private boolean mDidSearchInvolvePromo;
private boolean mWasSearchContentViewSeen;
private boolean mIsPromoActive;
private boolean mHasExpanded;
private boolean mHasMaximized;
private boolean mHasExitedPeeking;
private boolean mHasExitedExpanded;
private boolean mHasExitedMaximized;
private boolean mIsSerpNavigation;
private boolean mWasActivatedByTap;
private boolean mIsSearchPanelFullyPreloaded;
private boolean mWasIconSpriteAnimated;
private boolean mWasPanelOpenedBeyondPeek;
private boolean mWasSelectionPartOfUrl;
private boolean mWasContextualCardsDataShown;
private boolean mWasSelectionAllCaps;
private boolean mDidSelectionStartWithCapital;
// Whether any Tap suppression heuristic was satisfied when the panel was shown.
private boolean mWasAnyHeuristicSatisfiedOnPanelShow;
// Time when the panel was triggered (not reset by a chained search).
// Panel transitions are animated so mPanelTriggerTimeNs will be less than mFirstPeekTimeNs.
private long mPanelTriggerTimeNs;
// Time when the panel peeks into view (not reset by a chained search).
// Used to log total time the panel is showing (not closed).
private long mFirstPeekTimeNs;
// Time when the panel contents come into view (when the panel is opened).
// Used to log preload effectiveness info -- additional time needed to fully render the
// content in the overlay.
private long mContentFirstViewTimeNs;
// Time when a search request was started. Reset by chained searches.
// Used to log the time it takes for a Search Result to become available.
private long mSearchRequestStartTimeNs;
// Time when the panel was opened beyond peeked. Reset when the panel is closed.
// Used to log how long the panel was open.
private long mPanelOpenedBeyondPeekTimeNs;
// The current set of heuristics that should be logged with results seen when the panel closes.
private ContextualSearchHeuristics mResultsSeenExperiments;
/**
* Log information when the panel's state has changed.
* @param fromState The state the panel is transitioning from.
* @param toState The state that the panel is transitioning to.
* @param reason The reason for the state change.
*/
public void onPanelStateChanged(PanelState fromState, PanelState toState,
StateChangeReason reason) {
// Note: the logging within this function includes the promo, unless specifically
// excluded.
boolean isStartingSearch = isStartingNewContextualSearch(toState, reason);
boolean isEndingSearch = isEndingContextualSearch(fromState, toState, isStartingSearch);
boolean isChained = isStartingSearch && isOngoingContextualSearch(fromState);
boolean isSameState = fromState == toState;
boolean isFirstExitFromPeeking = fromState == PanelState.PEEKED && !mHasExitedPeeking
&& (!isSameState || isStartingSearch);
boolean isFirstExitFromExpanded = fromState == PanelState.EXPANDED && !mHasExitedExpanded
&& !isSameState;
boolean isFirstExitFromMaximized = fromState == PanelState.MAXIMIZED && !mHasExitedMaximized
&& !isSameState;
boolean isFirstSearchView = isFirstExitFromPeeking && toState != PanelState.CLOSED;
boolean isContentVisible =
toState == PanelState.MAXIMIZED || toState == PanelState.EXPANDED;
boolean isExitingPanelOpenedBeyondPeeked = mWasPanelOpenedBeyondPeek && !isContentVisible;
// This variable is needed for logging and gets reset in an isStartingSearch block below,
// so a local copy is created before the reset.
boolean isSearchPanelFullyPreloaded = mIsSearchPanelFullyPreloaded;
if (toState == PanelState.CLOSED && mPanelTriggerTimeNs != 0
&& reason == StateChangeReason.BASE_PAGE_SCROLL) {
long durationMs =
(System.nanoTime() - mPanelTriggerTimeNs) / MILLISECONDS_TO_NANOSECONDS;
ContextualSearchUma.logDurationBetweenTriggerAndScroll(
durationMs, mWasSearchContentViewSeen);
}
if (isEndingSearch) {
long durationMs = (System.nanoTime() - mFirstPeekTimeNs) / MILLISECONDS_TO_NANOSECONDS;
ContextualSearchUma.logPanelViewDurationAction(durationMs);
if (!mDidSearchInvolvePromo) {
// Measure duration only when the promo is not involved.
ContextualSearchUma.logDuration(mWasSearchContentViewSeen, isChained, durationMs);
}
if (mIsPromoActive) {
// The user is exiting still in the promo, without choosing an option.
ContextualSearchUma.logPromoSeen(mWasSearchContentViewSeen, mWasActivatedByTap);
} else {
ContextualSearchUma.logResultsSeen(mWasSearchContentViewSeen, mWasActivatedByTap);
}
if (mWasSelectionPartOfUrl) {
ContextualSearchUma.logResultsSeenSelectionIsUrl(mWasSearchContentViewSeen,
mWasActivatedByTap);
}
if (mWasContextualCardsDataShown) {
ContextualSearchUma.logContextualCardsResultsSeen(mWasSearchContentViewSeen);
}
if (mWasSelectionAllCaps && mWasActivatedByTap) {
ContextualSearchUma.logAllCapsResultsSeen(mWasSearchContentViewSeen);
} else if (mDidSelectionStartWithCapital && mWasActivatedByTap) {
ContextualSearchUma.logStartedWithCapitalResultsSeen(mWasSearchContentViewSeen);
}
ContextualSearchUma.logBlacklistSeen(mBlacklistReason, mWasSearchContentViewSeen);
ContextualSearchUma.logIconSpriteAnimated(mWasIconSpriteAnimated,
mWasSearchContentViewSeen, mWasActivatedByTap);
if (mResultsSeenExperiments != null) {
mResultsSeenExperiments.logResultsSeen(
mWasSearchContentViewSeen, mWasActivatedByTap);
mResultsSeenExperiments = null;
}
if (mWasActivatedByTap) {
boolean wasAnySuppressionHeuristicSatisfied =
mWasAnyHeuristicSatisfiedOnPanelShow || mWasSelectionPartOfUrl
|| mWasSelectionAllCaps;
ContextualSearchUma.logAnyTapSuppressionHeuristicSatisfied(
mWasSearchContentViewSeen, wasAnySuppressionHeuristicSatisfied);
}
}
if (isExitingPanelOpenedBeyondPeeked) {
assert mPanelOpenedBeyondPeekTimeNs != 0;
long durationPanelOpen = (System.nanoTime() - mPanelOpenedBeyondPeekTimeNs)
/ MILLISECONDS_TO_NANOSECONDS;
ContextualSearchUma.logPanelOpenDuration(durationPanelOpen);
mPanelOpenedBeyondPeekTimeNs = 0;
mWasPanelOpenedBeyondPeek = false;
}
if (isStartingSearch) {
mFirstPeekTimeNs = System.nanoTime();
mContentFirstViewTimeNs = 0;
mIsSearchPanelFullyPreloaded = false;
mWasActivatedByTap = reason == StateChangeReason.TEXT_SELECT_TAP;
mBlacklistReason = BlacklistReason.NONE;
if (mWasActivatedByTap && mResultsSeenExperiments != null) {
mWasAnyHeuristicSatisfiedOnPanelShow =
mResultsSeenExperiments.isAnyConditionSatisfiedForAggregrateLogging();
} else {
mWasAnyHeuristicSatisfiedOnPanelShow = false;
}
}
if (isFirstSearchView) {
onSearchPanelFirstView();
}
// Log state changes. We only log the first transition to a state within a contextual
// search. Note that when a user clicks on a link on the search content view, this will
// trigger a transition to MAXIMIZED (SERP_NAVIGATION) followed by a transition to
// CLOSED (TAB_PROMOTION). For the purpose of logging, the reason for the second transition
// is reinterpreted to SERP_NAVIGATION, in order to distinguish it from a tab promotion
// caused when tapping on the Search Bar when the Panel is maximized.
StateChangeReason reasonForLogging =
mIsSerpNavigation ? StateChangeReason.SERP_NAVIGATION : reason;
if (isStartingSearch || isEndingSearch
|| (!mHasExpanded && toState == PanelState.EXPANDED)
|| (!mHasMaximized && toState == PanelState.MAXIMIZED)) {
ContextualSearchUma.logFirstStateEntry(fromState, toState, reasonForLogging);
}
// Note: CLOSED / UNDEFINED state exits are detected when a search that is not chained is
// starting.
if ((isStartingSearch && !isChained) || isFirstExitFromPeeking || isFirstExitFromExpanded
|| isFirstExitFromMaximized) {
ContextualSearchUma.logFirstStateExit(fromState, toState, reasonForLogging);
}
// Log individual user actions so they can be sequenced.
ContextualSearchUma.logPanelStateUserAction(toState, reasonForLogging);
// We can now modify the state.
if (isFirstExitFromPeeking) {
mHasExitedPeeking = true;
} else if (isFirstExitFromExpanded) {
mHasExitedExpanded = true;
} else if (isFirstExitFromMaximized) {
mHasExitedMaximized = true;
}
if (toState == PanelState.EXPANDED) {
mHasExpanded = true;
} else if (toState == PanelState.MAXIMIZED) {
mHasMaximized = true;
}
if (reason == StateChangeReason.SERP_NAVIGATION) {
mIsSerpNavigation = true;
}
if (isEndingSearch) {
if (mHasMaximized || mHasExpanded) {
ContextualSearchUma.logSerpLoadedOnClose(isSearchPanelFullyPreloaded);
}
mDidSearchInvolvePromo = false;
mWasSearchContentViewSeen = false;
mHasExpanded = false;
mHasMaximized = false;
mHasExitedPeeking = false;
mHasExitedExpanded = false;
mHasExitedMaximized = false;
mIsSerpNavigation = false;
mWasSelectionPartOfUrl = false;
mWasContextualCardsDataShown = false;
mWasSelectionAllCaps = false;
mDidSelectionStartWithCapital = false;
mWasAnyHeuristicSatisfiedOnPanelShow = false;
mPanelTriggerTimeNs = 0;
}
}
/**
* Sets the reason why the current selection was blacklisted.
* @param reason The given reason.
*/
public void setBlacklistReason(BlacklistReason reason) {
mBlacklistReason = reason;
}
/**
* Sets that the contextual search involved the promo.
*/
public void setDidSearchInvolvePromo() {
mDidSearchInvolvePromo = true;
}
/**
* Sets that the Search Content View was seen.
*/
public void setWasSearchContentViewSeen() {
mWasSearchContentViewSeen = true;
mWasPanelOpenedBeyondPeek = true;
mPanelOpenedBeyondPeekTimeNs = System.nanoTime();
}
/**
* Sets whether the promo is active.
*/
public void setIsPromoActive(boolean shown) {
mIsPromoActive = shown;
}
/**
* @param wasIconSpriteAnimated Whether the search provider icon sprite was animated.
*/
public void setWasIconSpriteAnimated(boolean wasIconSpriteAnimated) {
mWasIconSpriteAnimated = wasIconSpriteAnimated;
}
/**
* @param wasPartOfUrl Whether the selected text was part of a URL.
*/
public void setWasSelectionPartOfUrl(boolean wasPartOfUrl) {
mWasSelectionPartOfUrl = wasPartOfUrl;
}
/**
* @param wasContextualCardsDataShown Whether Contextual Cards data was shown in the Contextual
* Search Bar.
*/
public void setWasContextualCardsDataShown(boolean wasContextualCardsDataShown) {
mWasContextualCardsDataShown = wasContextualCardsDataShown;
}
/**
* Should be called when the panel first starts showing.
*/
public void onPanelTriggered() {
mPanelTriggerTimeNs = System.nanoTime();
}
/**
* @param selection The text that is selected when a selection is established.
*/
public void onSelectionEstablished(String selection) {
// In some locales, there is no concept of an upper or lower case letter. Account for this
// by checking that the selected text is not equalivalet to selection#toLowerCase().
mWasSelectionAllCaps = selection.equals(selection.toUpperCase(Locale.getDefault()))
&& !selection.equals(selection.toLowerCase(Locale.getDefault()));
String firstChar = String.valueOf(selection.charAt(0));
mDidSelectionStartWithCapital = firstChar.equals(
firstChar.toUpperCase(Locale.getDefault()))
&& !firstChar.equals(firstChar.toLowerCase(Locale.getDefault()));
}
/**
* Called to record the time when a search request started, for resolve and prefetch timing.
*/
public void onSearchRequestStarted() {
mSearchRequestStartTimeNs = System.nanoTime();
}
/**
* Called when a Search Term has been resolved.
*/
public void onSearchTermResolved() {
long durationMs =
(System.nanoTime() - mSearchRequestStartTimeNs) / MILLISECONDS_TO_NANOSECONDS;
ContextualSearchUma.logSearchTermResolutionDuration(durationMs);
}
/**
* Records timing information when the search results have fully loaded.
* @param wasPrefetch Whether the request was prefetch-enabled.
*/
public void onSearchResultsLoaded(boolean wasPrefetch) {
if (mHasExpanded || mHasMaximized) {
// Already opened, log how long it took.
assert mContentFirstViewTimeNs != 0;
long durationMs =
(System.nanoTime() - mContentFirstViewTimeNs) / MILLISECONDS_TO_NANOSECONDS;
logSearchPanelLoadDuration(wasPrefetch, durationMs);
}
// Not yet opened, wait till an open to log.
mIsSearchPanelFullyPreloaded = true;
}
/**
* Called after the panel has navigated to prefetched Search Results.
* This is the point where the search result starts to render in the panel.
*/
public void onPanelNavigatedToPrefetchedSearch(boolean didResolve) {
long durationMs =
(System.nanoTime() - mSearchRequestStartTimeNs) / MILLISECONDS_TO_NANOSECONDS;
ContextualSearchUma.logPrefetchedSearchNavigatedDuration(durationMs, didResolve);
}
/**
* Sets the experiments to log with results seen.
* @param resultsSeenExperiments The experiments to log when the panel results are known.
*/
public void setResultsSeenExperiments(ContextualSearchHeuristics resultsSeenExperiments) {
mResultsSeenExperiments = resultsSeenExperiments;
}
/**
* Records timing information when the search panel has been viewed for the first time.
*/
private void onSearchPanelFirstView() {
if (mIsSearchPanelFullyPreloaded) {
// Already fully pre-loaded, record a wait of 0 milliseconds.
logSearchPanelLoadDuration(true, 0);
} else {
// Start a loading timer.
mContentFirstViewTimeNs = System.nanoTime();
}
}
/**
* Determine whether a new contextual search is starting.
* @param toState The contextual search state that will be transitioned to.
* @param reason The reason for the search state transition.
* @return Whether a new contextual search is starting.
*/
private boolean isStartingNewContextualSearch(PanelState toState, StateChangeReason reason) {
return toState == PanelState.PEEKED
&& (reason == StateChangeReason.TEXT_SELECT_TAP
|| reason == StateChangeReason.TEXT_SELECT_LONG_PRESS);
}
/**
* Determine whether a contextual search is ending.
* @param fromState The contextual search state that will be transitioned from.
* @param toState The contextual search state that will be transitioned to.
* @param isStartingSearch Whether a new contextual search is starting.
* @return Whether a contextual search is ending.
*/
private boolean isEndingContextualSearch(PanelState fromState, PanelState toState,
boolean isStartingSearch) {
return isOngoingContextualSearch(fromState)
&& (toState == PanelState.CLOSED || isStartingSearch);
}
/**
* @param fromState The state the panel is transitioning from.
* @return Whether there is an ongoing contextual search.
*/
private boolean isOngoingContextualSearch(PanelState fromState) {
return fromState != PanelState.UNDEFINED && fromState != PanelState.CLOSED;
}
/**
* Logs the duration the user waited for the search panel to fully load, once it was opened.
* @param wasPrefetch Whether the load included prefetch.
* @param durationMs The duration to log.
*/
private void logSearchPanelLoadDuration(boolean wasPrefetch, long durationMs) {
ContextualSearchUma.logSearchPanelLoadDuration(wasPrefetch, durationMs);
}
}