// 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.contextualsearch;

import android.util.Pair;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
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.preferences.PrefServiceBridge;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Centralizes UMA data collection for Contextual Search. All calls must be made from the UI thread.
 */
public class ContextualSearchUma {
    // Constants to use for the original selection gesture
    private static final boolean LONG_PRESS = false;
    private static final boolean TAP = true;

    // Constants used to log UMA "enum" histograms about the Contextual Search's preference state.
    private static final int PREFERENCE_UNINITIALIZED = 0;
    private static final int PREFERENCE_ENABLED = 1;
    private static final int PREFERENCE_DISABLED = 2;
    private static final int PREFERENCE_HISTOGRAM_BOUNDARY = 3;

    // Constants used to log UMA "enum" histograms about whether search results were seen.
    private static final int RESULTS_SEEN = 0;
    private static final int RESULTS_NOT_SEEN = 1;
    private static final int RESULTS_SEEN_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms about whether the selection is valid.
    private static final int SELECTION_VALID = 0;
    private static final int SELECTION_INVALID = 1;
    private static final int SELECTION_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms about a request's outcome.
    private static final int REQUEST_NOT_FAILED = 0;
    private static final int REQUEST_FAILED = 1;
    private static final int REQUEST_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms about the panel's state transitions.
    // Entry code: first entry into CLOSED.
    private static final int ENTER_CLOSED_FROM_OTHER = 0;
    private static final int ENTER_CLOSED_FROM_PEEKED_BACK_PRESS = 1;
    private static final int ENTER_CLOSED_FROM_PEEKED_BASE_PAGE_SCROLL = 2;
    private static final int ENTER_CLOSED_FROM_PEEKED_TEXT_SELECT_TAP = 3;
    private static final int ENTER_CLOSED_FROM_EXPANDED_BACK_PRESS = 4;
    private static final int ENTER_CLOSED_FROM_EXPANDED_BASE_PAGE_TAP = 5;
    private static final int ENTER_CLOSED_FROM_EXPANDED_FLING = 6;
    private static final int ENTER_CLOSED_FROM_MAXIMIZED_BACK_PRESS = 7;
    private static final int ENTER_CLOSED_FROM_MAXIMIZED_FLING = 8;
    private static final int ENTER_CLOSED_FROM_MAXIMIZED_TAB_PROMOTION = 9;
    private static final int ENTER_CLOSED_FROM_MAXIMIZED_SERP_NAVIGATION = 10;
    private static final int ENTER_CLOSED_FROM_BOUNDARY = 11;

    // Entry code: first entry into PEEKED.
    private static final int ENTER_PEEKED_FROM_OTHER = 0;
    private static final int ENTER_PEEKED_FROM_CLOSED_TEXT_SELECT_TAP = 1;
    private static final int ENTER_PEEKED_FROM_CLOSED_EXT_SELECT_LONG_PRESS = 2;
    private static final int ENTER_PEEKED_FROM_PEEKED_TEXT_SELECT_TAP = 3;
    private static final int ENTER_PEEKED_FROM_PEEKED_TEXT_SELECT_LONG_PRESS = 4;
    private static final int ENTER_PEEKED_FROM_EXPANDED_SEARCH_BAR_TAP = 5;
    private static final int ENTER_PEEKED_FROM_EXPANDED_SWIPE = 6;
    private static final int ENTER_PEEKED_FROM_EXPANDED_FLING = 7;
    private static final int ENTER_PEEKED_FROM_MAXIMIZED_SWIPE = 8;
    private static final int ENTER_PEEKED_FROM_MAXIMIZED_FLING = 9;
    private static final int ENTER_PEEKED_FROM_BOUNDARY = 10;

    // Entry code: first entry into EXPANDED.
    private static final int ENTER_EXPANDED_FROM_OTHER = 0;
    private static final int ENTER_EXPANDED_FROM_PEEKED_SEARCH_BAR_TAP = 1;
    private static final int ENTER_EXPANDED_FROM_PEEKED_SWIPE = 2;
    private static final int ENTER_EXPANDED_FROM_PEEKED_FLING = 3;
    private static final int ENTER_EXPANDED_FROM_MAXIMIZED_SWIPE = 4;
    private static final int ENTER_EXPANDED_FROM_MAXIMIZED_FLING = 5;
    private static final int ENTER_EXPANDED_FROM_BOUNDARY = 6;

    // Entry code: first entry into MAXIMIZED.
    private static final int ENTER_MAXIMIZED_FROM_OTHER = 0;
    private static final int ENTER_MAXIMIZED_FROM_PEEKED_SWIPE = 1;
    private static final int ENTER_MAXIMIZED_FROM_PEEKED_FLING = 2;
    private static final int ENTER_MAXIMIZED_FROM_EXPANDED_SWIPE = 3;
    private static final int ENTER_MAXIMIZED_FROM_EXPANDED_FLING = 4;
    private static final int ENTER_MAXIMIZED_FROM_EXPANDED_SERP_NAVIGATION = 5;
    private static final int ENTER_MAXIMIZED_FROM_BOUNDARY = 6;

    // Exit code: first exit from CLOSED (or UNDEFINED).
    private static final int EXIT_CLOSED_TO_OTHER = 0;
    private static final int EXIT_CLOSED_TO_PEEKED_TEXT_SELECT_TAP = 1;
    private static final int EXIT_CLOSED_TO_PEEKED_TEXT_SELECT_LONG_PRESS = 2;
    private static final int EXIT_CLOSED_TO_BOUNDARY = 3;

    // Exit code: first exit from PEEKED.
    private static final int EXIT_PEEKED_TO_OTHER = 0;
    private static final int EXIT_PEEKED_TO_CLOSED_BACK_PRESS = 1;
    private static final int EXIT_PEEKED_TO_CLOSED_BASE_PAGE_SCROLL = 2;
    private static final int EXIT_PEEKED_TO_CLOSED_TEXT_SELECT_TAP = 3;
    private static final int EXIT_PEEKED_TO_PEEKED_TEXT_SELECT_TAP = 4;
    private static final int EXIT_PEEKED_TO_PEEKED_TEXT_SELECT_LONG_PRESS = 5;
    private static final int EXIT_PEEKED_TO_EXPANDED_SEARCH_BAR_TAP = 6;
    private static final int EXIT_PEEKED_TO_EXPANDED_SWIPE = 7;
    private static final int EXIT_PEEKED_TO_EXPANDED_FLING = 8;
    private static final int EXIT_PEEKED_TO_MAXIMIZED_SWIPE = 9;
    private static final int EXIT_PEEKED_TO_MAXIMIZED_FLING = 10;
    private static final int EXIT_PEEKED_TO_BOUNDARY = 11;

    // Exit code: first exit from EXPANDED.
    private static final int EXIT_EXPANDED_TO_OTHER = 0;
    private static final int EXIT_EXPANDED_TO_CLOSED_BACK_PRESS = 1;
    private static final int EXIT_EXPANDED_TO_CLOSED_BASE_PAGE_TAP = 2;
    private static final int EXIT_EXPANDED_TO_CLOSED_FLING = 3;
    private static final int EXIT_EXPANDED_TO_PEEKED_SEARCH_BAR_TAP = 4;
    private static final int EXIT_EXPANDED_TO_PEEKED_SWIPE = 5;
    private static final int EXIT_EXPANDED_TO_PEEKED_FLING = 6;
    private static final int EXIT_EXPANDED_TO_MAXIMIZED_SWIPE = 7;
    private static final int EXIT_EXPANDED_TO_MAXIMIZED_FLING = 8;
    private static final int EXIT_EXPANDED_TO_MAXIMIZED_SERP_NAVIGATION = 9;
    private static final int EXIT_EXPANDED_TO_BOUNDARY = 10;

    // Exit code: first exit from MAXIMIZED.
    private static final int EXIT_MAXIMIZED_TO_OTHER = 0;
    private static final int EXIT_MAXIMIZED_TO_CLOSED_BACK_PRESS = 1;
    private static final int EXIT_MAXIMIZED_TO_CLOSED_FLING = 2;
    private static final int EXIT_MAXIMIZED_TO_CLOSED_TAB_PROMOTION = 3;
    private static final int EXIT_MAXIMIZED_TO_CLOSED_SERP_NAVIGATION = 4;
    private static final int EXIT_MAXIMIZED_TO_PEEKED_SWIPE = 5;
    private static final int EXIT_MAXIMIZED_TO_PEEKED_FLING = 6;
    private static final int EXIT_MAXIMIZED_TO_EXPANDED_SWIPE = 7;
    private static final int EXIT_MAXIMIZED_TO_EXPANDED_FLING = 8;
    private static final int EXIT_MAXIMIZED_TO_BOUNDARY = 9;

    // Constants used to log UMA "enum" histograms with details about whether search results
    // were seen, and what the original triggering gesture was.
    private static final int RESULTS_SEEN_FROM_TAP = 0;
    private static final int RESULTS_NOT_SEEN_FROM_TAP = 1;
    private static final int RESULTS_SEEN_FROM_LONG_PRESS = 2;
    private static final int RESULTS_NOT_SEEN_FROM_LONG_PRESS = 3;
    private static final int RESULTS_BY_GESTURE_BOUNDARY = 4;

    // Constants used to log UMA "enum" histograms with details about whether search results
    // were seen, and whether any existing tap suppression heuristics were satisfied.
    private static final int RESULTS_SEEN_SUPPRESSION_HEURSTIC_SATISFIED = 0;
    private static final int RESULTS_NOT_SEEN_SUPPRESSION_HEURSTIC_SATISFIED = 1;
    private static final int RESULTS_SEEN_SUPPRESSION_HEURSTIC_NOT_SATISFIED = 2;
    private static final int RESULTS_NOT_SEEN_SUPPRESSION_HEURSTIC_NOT_SATISFIED = 3;
    private static final int RESULTS_SEEN_SUPPRESSION_BOUNDARY = 4;

    // Constants used to log UMA "enum" histograms with details about the Peek Promo Outcome.
    private static final int PEEK_PROMO_OUTCOME_SEEN_OPENED = 0;
    private static final int PEEK_PROMO_OUTCOME_SEEN_NOT_OPENED = 1;
    private static final int PEEK_PROMO_OUTCOME_NOT_SEEN_OPENED = 2;
    private static final int PEEK_PROMO_OUTCOME_NOT_SEEN_NOT_OPENED = 3;
    private static final int PEEK_PROMO_OUTCOME_BOUNDARY = 4;

    // Constants used to log UMA "enum" histograms with details about whether search results
    // were seen, and what the original triggering gesture was.
    private static final int PROMO_ENABLED_FROM_TAP = 0;
    private static final int PROMO_DISABLED_FROM_TAP = 1;
    private static final int PROMO_UNDECIDED_FROM_TAP = 2;
    private static final int PROMO_ENABLED_FROM_LONG_PRESS = 3;
    private static final int PROMO_DISABLED_FROM_LONG_PRESS = 4;
    private static final int PROMO_UNDECIDED_FROM_LONG_PRESS = 5;
    private static final int PROMO_BY_GESTURE_BOUNDARY = 6;

    // Constants used to log UMA "enum" histograms with summary counts for SERP loading times.
    private static final int PREFETCHED_PARIALLY_LOADED = 0;
    private static final int PREFETCHED_FULLY_LOADED = 1;
    private static final int NOT_PREFETCHED = 2;
    private static final int PREFETCH_BOUNDARY = 3;

    // Constants used to log UMA "enum" histograms for HTTP / HTTPS.
    private static final int PROTOCOL_IS_HTTP = 0;
    private static final int PROTOCOL_NOT_HTTP = 1;
    private static final int PROTOCOL_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms for single / multi-word.
    private static final int RESOLVED_SINGLE_WORD = 0;
    private static final int RESOLVED_MULTI_WORD = 1;
    private static final int RESOLVED_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms for partially / fully loaded.
    private static final int PARTIALLY_LOADED = 0;
    private static final int FULLY_LOADED = 1;
    private static final int LOADED_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms for triggering the Translate Onebox.
    private static final int DID_FORCE_TRANSLATE = 0;
    private static final int WOULD_FORCE_TRANSLATE = 1;
    private static final int FORCE_TRANSLATE_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms with details about whether the search
    // provider sprite icon was animated, whether search results were seen and the triggering
    // gesture. All new values should be inserted right before ICON_SPRITE_BOUNDARY.
    private static final int ICON_SPRITE_ANIMATED_RESULTS_SEEN_FROM_TAP = 0;
    private static final int ICON_SPRITE_ANIMATED_RESULTS_NOT_SEEN_FROM_TAP = 1;
    private static final int ICON_SPRITE_NOT_ANIMATED_RESULTS_SEEN_FROM_TAP = 2;
    private static final int ICON_SPRITE_NOT_ANIMATED_RESULTS_NOT_SEEN_FROM_TAP = 3;
    private static final int ICON_SPRITE_ANIMATED_RESULTS_SEEN_FROM_LONG_PRESS = 4;
    private static final int ICON_SPRITE_ANIMATED_RESULTS_NOT_SEEN_FROM_LONG_PRESS = 5;
    private static final int ICON_SPRITE_NOT_ANIMATED_RESULTS_SEEN_FROM_LONG_PRESS = 6;
    private static final int ICON_SPRITE_NOT_ANIMATED_RESULTS_NOT_SEEN_FROM_LONG_PRESS = 7;
    private static final int ICON_SPRITE_BOUNDARY = 8;

    // Constants used to log UMA "enum" histograms for any kind of Tap suppression.
    private static final int TAP_SUPPRESSED = 0;
    private static final int NOT_TAP_SUPPRESSED = 1;
    private static final int TAP_SUPPRESSED_BOUNDARY = 2;

    // Constants used to log UMA "enum" histograms for Quick Answers.
    private static final int QUICK_ANSWER_ACTIVATED_WAS_AN_ANSWER_SEEN = 0;
    private static final int QUICK_ANSWER_ACTIVATED_WAS_AN_ANSWER_NOT_SEEN = 1;
    private static final int QUICK_ANSWER_ACTIVATED_NOT_AN_ANSWER_SEEN = 2;
    private static final int QUICK_ANSWER_ACTIVATED_NOT_AN_ANSWER_NOT_SEEN = 3;
    private static final int QUICK_ANSWER_NOT_ACTIVATED_SEEN = 4;
    private static final int QUICK_ANSWER_NOT_ACTIVATED_NOT_SEEN = 5;
    private static final int QUICK_ANSWER_SEEN_BOUNDARY = 6;

    // Constants for "Bar Overlap" with triggering gesture, and whether the results were seen.
    private static final int BAR_OVERLAP_RESULTS_SEEN_FROM_TAP = 0;
    private static final int BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_TAP = 1;
    private static final int NO_BAR_OVERLAP_RESULTS_SEEN_FROM_TAP = 2;
    private static final int NO_BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_TAP = 3;
    private static final int BAR_OVERLAP_RESULTS_SEEN_FROM_LONG_PRESS = 4;
    private static final int BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_LONG_PRESS = 5;
    private static final int NO_BAR_OVERLAP_RESULTS_SEEN_FROM_LONG_PRESS = 6;
    private static final int NO_BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_LONG_PRESS = 7;
    private static final int BAR_OVERLAP_RESULTS_BOUNDARY = 8;

    // Constants for quick action intent resolution histogram.
    private static final int QUICK_ACTION_RESOLVE_FAILED = 0;
    private static final int QUICK_ACTION_RESOLVE_SINGLE = 1;
    private static final int QUICK_ACTION_RESOLVE_MULTIPLE = 2;
    private static final int QUICK_ACTION_RESOLVE_BOUNDARY = 3;

    /**
     * Key used in maps from {state, reason} to state entry (exit) logging code.
     */
    static class StateChangeKey {
        final PanelState mState;
        final StateChangeReason mReason;
        final int mHashCode;

        StateChangeKey(PanelState state, StateChangeReason reason) {
            mState = state;
            mReason = reason;
            mHashCode = 31 * state.hashCode() + reason.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof StateChangeKey)) {
                return false;
            }
            if (obj == this) {
                return true;
            }
            StateChangeKey other = (StateChangeKey) obj;
            return mState.equals(other.mState) && mReason.equals(other.mReason);
        }

        @Override
        public int hashCode() {
            return mHashCode;
        }
    }

    static class IconSpriteAnimationKey {
        final boolean mWasIconSpriteAnimated;
        final boolean mWasPanelSeen;
        final boolean mWasTap;
        final int mHashCode;

        IconSpriteAnimationKey(boolean wasIconSpriteAnimated, boolean wasPanelSeen,
                boolean wasTap) {
            mWasIconSpriteAnimated = wasIconSpriteAnimated;
            mWasPanelSeen = wasPanelSeen;
            mWasTap = wasTap;

            // HashCode logic generated by Eclipse.
            final int prime = 31;
            int result = 1;
            result = prime * result + (mWasIconSpriteAnimated ? 1231 : 1237);
            result = prime * result + (mWasPanelSeen ? 1231 : 1237);
            result = prime * result + (mWasTap ? 1231 : 1237);
            mHashCode = result;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof IconSpriteAnimationKey)) {
                return false;
            }
            if (obj == this) {
                return true;
            }
            IconSpriteAnimationKey other = (IconSpriteAnimationKey) obj;
            return other.mWasIconSpriteAnimated == mWasIconSpriteAnimated
                    && other.mWasPanelSeen == mWasPanelSeen
                    && other.mWasTap == mWasTap;
        }

        @Override
        public int hashCode() {
            return mHashCode;
        }
    }

    // TODO(donnd): switch from using Maps to some method that does not require creation of a key.

    // Entry code map: first entry into CLOSED.
    private static final Map<StateChangeKey, Integer> ENTER_CLOSED_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.BACK_PRESS),
                ENTER_CLOSED_FROM_PEEKED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.BASE_PAGE_SCROLL),
                ENTER_CLOSED_FROM_PEEKED_BASE_PAGE_SCROLL);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_TAP),
                ENTER_CLOSED_FROM_PEEKED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.BACK_PRESS),
                ENTER_CLOSED_FROM_EXPANDED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.BASE_PAGE_TAP),
                ENTER_CLOSED_FROM_EXPANDED_BASE_PAGE_TAP);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.FLING),
                ENTER_CLOSED_FROM_EXPANDED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.BACK_PRESS),
                ENTER_CLOSED_FROM_MAXIMIZED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.FLING),
                ENTER_CLOSED_FROM_MAXIMIZED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.TAB_PROMOTION),
                ENTER_CLOSED_FROM_MAXIMIZED_TAB_PROMOTION);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SERP_NAVIGATION),
                ENTER_CLOSED_FROM_MAXIMIZED_SERP_NAVIGATION);
        ENTER_CLOSED_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Entry code map: first entry into PEEKED.
    private static final Map<StateChangeKey, Integer> ENTER_PEEKED_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        // Note: we don't distinguish entering PEEKED from UNDEFINED / CLOSED.
        codes.put(new StateChangeKey(PanelState.UNDEFINED, StateChangeReason.TEXT_SELECT_TAP),
                ENTER_PEEKED_FROM_CLOSED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.UNDEFINED,
                StateChangeReason.TEXT_SELECT_LONG_PRESS),
                ENTER_PEEKED_FROM_CLOSED_EXT_SELECT_LONG_PRESS);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.TEXT_SELECT_TAP),
                ENTER_PEEKED_FROM_CLOSED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.TEXT_SELECT_LONG_PRESS),
                ENTER_PEEKED_FROM_CLOSED_EXT_SELECT_LONG_PRESS);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_TAP),
                ENTER_PEEKED_FROM_PEEKED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_LONG_PRESS),
                ENTER_PEEKED_FROM_PEEKED_TEXT_SELECT_LONG_PRESS);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SEARCH_BAR_TAP),
                ENTER_PEEKED_FROM_EXPANDED_SEARCH_BAR_TAP);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SWIPE),
                ENTER_PEEKED_FROM_EXPANDED_SWIPE);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.FLING),
                ENTER_PEEKED_FROM_EXPANDED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SWIPE),
                ENTER_PEEKED_FROM_MAXIMIZED_SWIPE);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.FLING),
                ENTER_PEEKED_FROM_MAXIMIZED_FLING);
        ENTER_PEEKED_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Entry code map: first entry into EXPANDED.
    private static final Map<StateChangeKey, Integer> ENTER_EXPANDED_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SEARCH_BAR_TAP),
                ENTER_EXPANDED_FROM_PEEKED_SEARCH_BAR_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SWIPE),
                ENTER_EXPANDED_FROM_PEEKED_SWIPE);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.FLING),
                ENTER_EXPANDED_FROM_PEEKED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SWIPE),
                ENTER_EXPANDED_FROM_MAXIMIZED_SWIPE);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.FLING),
                ENTER_EXPANDED_FROM_MAXIMIZED_FLING);
        ENTER_EXPANDED_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Entry code map: first entry into MAXIMIZED.
    private static final Map<StateChangeKey, Integer> ENTER_MAXIMIZED_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SWIPE),
                ENTER_MAXIMIZED_FROM_PEEKED_SWIPE);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.FLING),
                ENTER_MAXIMIZED_FROM_PEEKED_FLING);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SWIPE),
                ENTER_MAXIMIZED_FROM_EXPANDED_SWIPE);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.FLING),
                ENTER_MAXIMIZED_FROM_EXPANDED_FLING);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SERP_NAVIGATION),
                ENTER_MAXIMIZED_FROM_EXPANDED_SERP_NAVIGATION);
        ENTER_MAXIMIZED_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Exit code map: first exit from CLOSED.
    private static final Map<StateChangeKey, Integer> EXIT_CLOSED_TO_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_TAP),
                EXIT_CLOSED_TO_PEEKED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_LONG_PRESS),
                EXIT_CLOSED_TO_PEEKED_TEXT_SELECT_LONG_PRESS);
        EXIT_CLOSED_TO_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Exit code map: first exit from PEEKED.
    private static final Map<StateChangeKey, Integer> EXIT_PEEKED_TO_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BACK_PRESS),
                EXIT_PEEKED_TO_CLOSED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BASE_PAGE_SCROLL),
                EXIT_PEEKED_TO_CLOSED_BASE_PAGE_SCROLL);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BASE_PAGE_TAP),
                EXIT_PEEKED_TO_CLOSED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_TAP),
                EXIT_PEEKED_TO_PEEKED_TEXT_SELECT_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.TEXT_SELECT_LONG_PRESS),
                EXIT_PEEKED_TO_PEEKED_TEXT_SELECT_LONG_PRESS);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SEARCH_BAR_TAP),
                EXIT_PEEKED_TO_EXPANDED_SEARCH_BAR_TAP);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SWIPE),
                EXIT_PEEKED_TO_EXPANDED_SWIPE);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.FLING),
                EXIT_PEEKED_TO_EXPANDED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SWIPE),
                EXIT_PEEKED_TO_MAXIMIZED_SWIPE);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.FLING),
                EXIT_PEEKED_TO_MAXIMIZED_FLING);
        EXIT_PEEKED_TO_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Exit code map: first exit from EXPANDED.
    private static final Map<StateChangeKey, Integer> EXIT_EXPANDED_TO_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BACK_PRESS),
                EXIT_EXPANDED_TO_CLOSED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BASE_PAGE_TAP),
                EXIT_EXPANDED_TO_CLOSED_BASE_PAGE_TAP);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.FLING),
                EXIT_EXPANDED_TO_CLOSED_FLING);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SEARCH_BAR_TAP),
                EXIT_EXPANDED_TO_PEEKED_SEARCH_BAR_TAP);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SWIPE),
                EXIT_EXPANDED_TO_PEEKED_SWIPE);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.FLING),
                EXIT_EXPANDED_TO_PEEKED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SWIPE),
                EXIT_EXPANDED_TO_MAXIMIZED_SWIPE);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.FLING),
                EXIT_EXPANDED_TO_MAXIMIZED_FLING);
        codes.put(new StateChangeKey(PanelState.MAXIMIZED, StateChangeReason.SERP_NAVIGATION),
                EXIT_EXPANDED_TO_MAXIMIZED_SERP_NAVIGATION);
        EXIT_EXPANDED_TO_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // Exit code map: first exit from MAXIMIZED.
    private static final Map<StateChangeKey, Integer> EXIT_MAXIMIZED_TO_STATE_CHANGE_CODES;
    static {
        Map<StateChangeKey, Integer> codes = new HashMap<StateChangeKey, Integer>();
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.BACK_PRESS),
                EXIT_MAXIMIZED_TO_CLOSED_BACK_PRESS);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.FLING),
                EXIT_MAXIMIZED_TO_CLOSED_FLING);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.TAB_PROMOTION),
                EXIT_MAXIMIZED_TO_CLOSED_TAB_PROMOTION);
        codes.put(new StateChangeKey(PanelState.CLOSED, StateChangeReason.SERP_NAVIGATION),
                EXIT_MAXIMIZED_TO_CLOSED_SERP_NAVIGATION);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.SWIPE),
                EXIT_MAXIMIZED_TO_PEEKED_SWIPE);
        codes.put(new StateChangeKey(PanelState.PEEKED, StateChangeReason.FLING),
                EXIT_MAXIMIZED_TO_PEEKED_FLING);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.SWIPE),
                EXIT_MAXIMIZED_TO_EXPANDED_SWIPE);
        codes.put(new StateChangeKey(PanelState.EXPANDED, StateChangeReason.FLING),
                EXIT_MAXIMIZED_TO_EXPANDED_FLING);
        EXIT_MAXIMIZED_TO_STATE_CHANGE_CODES = Collections.unmodifiableMap(codes);
    }

    // "Seen by gesture" code map: logged on first exit from expanded panel, or promo,
    // broken down by gesture.
    private static final Map<Pair<Boolean, Boolean>, Integer> SEEN_BY_GESTURE_CODES;
    static {
        final boolean unseen = false;
        final boolean seen = true;
        Map<Pair<Boolean, Boolean>, Integer> codes = new HashMap<Pair<Boolean, Boolean>, Integer>();
        codes.put(new Pair<Boolean, Boolean>(seen, TAP), RESULTS_SEEN_FROM_TAP);
        codes.put(new Pair<Boolean, Boolean>(unseen, TAP), RESULTS_NOT_SEEN_FROM_TAP);
        codes.put(new Pair<Boolean, Boolean>(seen, LONG_PRESS), RESULTS_SEEN_FROM_LONG_PRESS);
        codes.put(new Pair<Boolean, Boolean>(unseen, LONG_PRESS), RESULTS_NOT_SEEN_FROM_LONG_PRESS);
        SEEN_BY_GESTURE_CODES = Collections.unmodifiableMap(codes);
    }

    // "Promo outcome by gesture" code map: logged on exit from promo, broken down by gesture.
    private static final Map<Pair<Integer, Boolean>, Integer> PROMO_BY_GESTURE_CODES;
    static {
        Map<Pair<Integer, Boolean>, Integer> codes =
                new HashMap<Pair<Integer, Boolean>, Integer>();
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_ENABLED, TAP), PROMO_ENABLED_FROM_TAP);
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_DISABLED, TAP), PROMO_DISABLED_FROM_TAP);
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_UNINITIALIZED, TAP),
                PROMO_UNDECIDED_FROM_TAP);
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_ENABLED, LONG_PRESS),
                PROMO_ENABLED_FROM_LONG_PRESS);
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_DISABLED, LONG_PRESS),
                PROMO_DISABLED_FROM_LONG_PRESS);
        codes.put(new Pair<Integer, Boolean>(PREFERENCE_UNINITIALIZED, LONG_PRESS),
                PROMO_UNDECIDED_FROM_LONG_PRESS);
        PROMO_BY_GESTURE_CODES = Collections.unmodifiableMap(codes);
    }

    // Icon sprite animation code mapped: logged when ending a contextual search.
    private static final Map<IconSpriteAnimationKey, Integer> ICON_SPRITE_ANIMATION_CODES;
    static {
        Map<IconSpriteAnimationKey, Integer> codes = new HashMap<IconSpriteAnimationKey, Integer>();
        codes.put(new IconSpriteAnimationKey(true, true, true),
                ICON_SPRITE_ANIMATED_RESULTS_SEEN_FROM_TAP);
        codes.put(new IconSpriteAnimationKey(true, false, true),
                ICON_SPRITE_ANIMATED_RESULTS_NOT_SEEN_FROM_TAP);
        codes.put(new IconSpriteAnimationKey(false, true, true),
                ICON_SPRITE_NOT_ANIMATED_RESULTS_SEEN_FROM_TAP);
        codes.put(new IconSpriteAnimationKey(false, false, true),
                ICON_SPRITE_NOT_ANIMATED_RESULTS_NOT_SEEN_FROM_TAP);
        codes.put(new IconSpriteAnimationKey(true, true, false),
                ICON_SPRITE_ANIMATED_RESULTS_SEEN_FROM_LONG_PRESS);
        codes.put(new IconSpriteAnimationKey(true, false, false),
                ICON_SPRITE_ANIMATED_RESULTS_NOT_SEEN_FROM_LONG_PRESS);
        codes.put(new IconSpriteAnimationKey(false, true, false),
                ICON_SPRITE_NOT_ANIMATED_RESULTS_SEEN_FROM_LONG_PRESS);
        codes.put(new IconSpriteAnimationKey(false, false, false),
                ICON_SPRITE_NOT_ANIMATED_RESULTS_NOT_SEEN_FROM_LONG_PRESS);
        ICON_SPRITE_ANIMATION_CODES = Collections.unmodifiableMap(codes);
    }

    /**
     * Logs the state of the Contextual Search preference. This function should be called if the
     * Contextual Search feature is active, and will track the different preference settings
     * (disabled, enabled or uninitialized). Calling more than once is fine.
     */
    public static void logPreferenceState() {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchPreferenceState",
                getPreferenceValue(), PREFERENCE_HISTOGRAM_BOUNDARY);
    }

    /**
     * Logs the given number of promo taps remaining.  Should be called only for users that
     * are still undecided.
     * @param promoTapsRemaining The number of taps remaining (should not be negative).
     */
    public static void logPromoTapsRemaining(int promoTapsRemaining) {
        if (promoTapsRemaining >= 0) {
            RecordHistogram.recordCountHistogram("Search.ContextualSearchPromoTapsRemaining",
                    promoTapsRemaining);
        }
    }

    /**
     * Logs the historic number of times that a Tap gesture triggered the peeking promo
     * for users that have never opened the panel.  This should be called periodically for
     * undecided users only.
     * @param promoTaps The historic number of taps that have caused the peeking bar for the promo,
     *        for users that have never opened the panel.
     */
    public static void logPromoTapsForNeverOpened(int promoTaps) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchPromoTapsForNeverOpened",
                promoTaps);
    }

    /**
     * Logs the historic number of times that a Tap gesture triggered the peeking promo before
     * the user ever opened the panel.  This should be called periodically for all users.
     * @param promoTaps The historic number of taps that have caused the peeking bar for the promo
     *        before the first open of the panel, for all users that have ever opened the panel.
     */
    public static void logPromoTapsBeforeFirstOpen(int promoTaps) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchPromoTapsBeforeFirstOpen",
                promoTaps);
    }

    /**
     * Records the total count of times the promo panel has *ever* been opened.  This should only
     * be called when the user is still undecided.
     * @param count The total historic count of times the panel has ever been opened for the
     *        current user.
     */
    public static void logPromoOpenCount(int count) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchPromoOpenCount", count);
    }

    /**
     * Logs the number of taps that have been counted since the user last opened the panel, for
     * undecided users.
     * @param tapsSinceOpen The number of taps to log.
     */
    public static void logTapsSinceOpenForUndecided(int tapsSinceOpen) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchTapsSinceOpenUndecided",
                tapsSinceOpen);
    }

    /**
     * Logs the number of taps that have been counted since the user last opened the panel, for
     * decided users.
     * @param tapsSinceOpen The number of taps to log.
     */
    public static void logTapsSinceOpenForDecided(int tapsSinceOpen) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchTapsSinceOpenDecided",
                tapsSinceOpen);
    }

    /**
     * Logs whether the Search Term was single or multiword.
     * @param isSingleWord Whether the resolved search term is a single word or not.
     */
    public static void logSearchTermResolvedWords(boolean isSingleWord) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchResolvedTermWords",
                isSingleWord ? RESOLVED_SINGLE_WORD : RESOLVED_MULTI_WORD, RESOLVED_BOUNDARY);
    }

    /**
     * Logs whether the base page was using the HTTP protocol or not.
     * @param isHttpBasePage Whether the base page was using the HTTP protocol or not (should
     *        be false for HTTPS or other URIs).
     */
    public static void logBasePageProtocol(boolean isHttpBasePage) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchBasePageProtocol",
                isHttpBasePage ? PROTOCOL_IS_HTTP : PROTOCOL_NOT_HTTP, PROTOCOL_BOUNDARY);
    }

    /**
     * Logs changes to the Contextual Search preference, aside from those resulting from the first
     * run flow.
     * @param enabled Whether the preference is being enabled or disabled.
     */
    public static void logPreferenceChange(boolean enabled) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchPreferenceStateChange",
                enabled ? PREFERENCE_ENABLED : PREFERENCE_DISABLED, PREFERENCE_HISTOGRAM_BOUNDARY);
    }

    /**
     * Logs the number of times the Peek Promo was seen.
     * @param count Number of times the Peek Promo was seen.
     * @param hasOpenedPanel Whether the Panel was opened.
     */
    public static void logPeekPromoShowCount(int count, boolean hasOpenedPanel) {
        RecordHistogram.recordCountHistogram("Search.ContextualSearchPeekPromoCount", count);
        if (hasOpenedPanel) {
            RecordHistogram.recordCountHistogram(
                    "Search.ContextualSearchPeekPromoCountUntilOpened", count);
        }
    }

    /**
     * Logs the Peek Promo Outcome.
     * @param wasPromoSeen Whether the Peek Promo was seen.
     * @param wouldHaveShownPromo Whether the Promo would have shown.
     * @param hasOpenedPanel Whether the Panel was opened.
     */
    public static void logPeekPromoOutcome(boolean wasPromoSeen, boolean wouldHaveShownPromo,
            boolean hasOpenedPanel) {
        int outcome = -1;
        if (wasPromoSeen) {
            outcome = hasOpenedPanel
                    ? PEEK_PROMO_OUTCOME_SEEN_OPENED : PEEK_PROMO_OUTCOME_SEEN_NOT_OPENED;
        } else if (wouldHaveShownPromo) {
            outcome = hasOpenedPanel
                    ? PEEK_PROMO_OUTCOME_NOT_SEEN_OPENED : PEEK_PROMO_OUTCOME_NOT_SEEN_NOT_OPENED;
        }

        if (outcome != -1) {
            RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchPeekPromoOutcome",
                    outcome, PEEK_PROMO_OUTCOME_BOUNDARY);
        }
    }

    /**
     * Logs the outcome of the Promo.
     * Logs multiple histograms; with and without the originating gesture.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     * @param wasMandatory Whether the Promo was mandatory.
     */
    public static void logPromoOutcome(boolean wasTap, boolean wasMandatory) {
        int preferenceCode = getPreferenceValue();
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchFirstRunFlowOutcome",
                preferenceCode, PREFERENCE_HISTOGRAM_BOUNDARY);

        int preferenceByGestureCode = getPromoByGestureStateCode(preferenceCode, wasTap);
        if (wasMandatory) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Search.ContextualSearchMandatoryPromoOutcomeByGesture",
                    preferenceByGestureCode, PROMO_BY_GESTURE_BOUNDARY);
        } else {
            RecordHistogram.recordEnumeratedHistogram(
                    "Search.ContextualSearchPromoOutcomeByGesture",
                    preferenceByGestureCode, PROMO_BY_GESTURE_BOUNDARY);
        }
    }

    /**
     * Logs the duration of a Contextual Search panel being viewed by the user.
     * @param wereResultsSeen Whether search results were seen.
     * @param isChained Whether the Contextual Search ended with the start of another.
     * @param durationMs The duration of the contextual search in milliseconds.
     */
    public static void logDuration(boolean wereResultsSeen, boolean isChained, long durationMs) {
        if (wereResultsSeen) {
            RecordHistogram.recordTimesHistogram("Search.ContextualSearchDurationSeen",
                    durationMs, TimeUnit.MILLISECONDS);
        } else if (isChained) {
            RecordHistogram.recordTimesHistogram("Search.ContextualSearchDurationUnseenChained",
                    durationMs, TimeUnit.MILLISECONDS);
        } else {
            RecordHistogram.recordTimesHistogram("Search.ContextualSearchDurationUnseen",
                    durationMs, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * Log the duration of finishing loading the SERP after the panel is opened.
     * @param wasPrefetch Whether the request was prefetch-enabled or not.
     * @param durationMs The duration of loading the SERP till completely loaded, in milliseconds.
     *        Note that this value will be 0 when the SERP is prefetched and the user waits a
     *        while before opening the panel.
     */
    public static void logSearchPanelLoadDuration(boolean wasPrefetch, long durationMs) {
        if (wasPrefetch) {
            RecordHistogram.recordMediumTimesHistogram("Search.ContextualSearchDurationPrefetched",
                    durationMs, TimeUnit.MILLISECONDS);
        } else {
            RecordHistogram.recordMediumTimesHistogram(
                    "Search.ContextualSearchDurationNonPrefetched", durationMs,
                    TimeUnit.MILLISECONDS);
        }

       // Also record a summary histogram with counts for each possibility.
        int code = !wasPrefetch ? NOT_PREFETCHED
                : (durationMs == 0 ? PREFETCHED_FULLY_LOADED : PREFETCHED_PARIALLY_LOADED);
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchPrefetchSummary",
                code, PREFETCH_BOUNDARY);
    }

    /**
     * Logs the duration from starting a search until the Search Term is resolved.
     * @param durationMs The duration to record.
     */
    public static void logSearchTermResolutionDuration(long durationMs) {
        RecordHistogram.recordMediumTimesHistogram(
                "Search.ContextualSearchResolutionDuration", durationMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Logs the duration from starting a prefetched search until the panel navigates to the results
     * and they start becoming viewable. Should be called only for searches that are prefetched.
     * @param durationMs The duration to record.
     * @param didResolve Whether a Search Term resolution was required as part of the loading.
     */
    public static void logPrefetchedSearchNavigatedDuration(long durationMs, boolean didResolve) {
        String histogramName = didResolve ? "Search.ContextualSearchResolvedSearchDuration"
                                          : "Search.ContextualSearchLiteralSearchDuration";
        RecordHistogram.recordMediumTimesHistogram(
                histogramName, durationMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Logs the duration from opening the panel beyond peek until the panel is closed.
     * @param durationMs The duration to record.
     */
    public static void logPanelOpenDuration(long durationMs) {
        RecordHistogram.recordMediumTimesHistogram(
                "Search.ContextualSearchPanelOpenDuration", durationMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Logs a user action for the duration of viewing the panel that describes the amount of time
     * the user viewed the bar and panel overall.
     * @param durationMs The duration to record.
     */
    public static void logPanelViewDurationAction(long durationMs) {
        if (durationMs < 1000) {
            RecordUserAction.record("ContextualSearch.ViewLessThanOneSecond");
        } else if (durationMs < 3000) {
            RecordUserAction.record("ContextualSearch.ViewOneToThreeSeconds");
        } else if (durationMs < 10000) {
            RecordUserAction.record("ContextualSearch.ViewThreeToTenSeconds");
        } else {
            RecordUserAction.record("ContextualSearch.ViewMoreThanTenSeconds");
        }
    }

    /**
     * Logs whether the promo was seen.
     * Logs multiple histograms, with and without the original triggering gesture.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     */
    public static void logPromoSeen(boolean wasPanelSeen, boolean wasTap) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchFirstRunPanelSeen",
                wasPanelSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN, RESULTS_SEEN_BOUNDARY);
        logHistogramByGesture(wasPanelSeen, wasTap, "Search.ContextualSearchPromoSeenByGesture");
    }

    /**
     * Logs whether search results were seen.
     * Logs multiple histograms; with and without the original triggering gesture.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     */
    public static void logResultsSeen(boolean wasPanelSeen, boolean wasTap) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchResultsSeen",
                wasPanelSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN, RESULTS_SEEN_BOUNDARY);
        logHistogramByGesture(wasPanelSeen, wasTap, "Search.ContextualSearchResultsSeenByGesture");
    }

    /**
     * Logs whether search results were seen when the selection was part of a URL.
     * Unlike ContextualSearchResultsSeen, this histogram is logged for both decided and undecided
     * users.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     */
    public static void logResultsSeenSelectionIsUrl(boolean wasPanelSeen, boolean wasTap) {
        int result = wasPanelSeen ? (wasTap ? RESULTS_SEEN_FROM_TAP : RESULTS_SEEN_FROM_LONG_PRESS)
                : (wasTap ? RESULTS_NOT_SEEN_FROM_TAP : RESULTS_NOT_SEEN_FROM_LONG_PRESS);
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchResultsSeenSelectionWasUrl", result,
                RESULTS_BY_GESTURE_BOUNDARY);
    }

    /**
     * Logs the whether the panel was seen and the type of the trigger and if Bar nearly overlapped.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture was a Tap or not.
     * @param wasBarOverlap Whether the trigger location overlapped the Bar area.
     */
    public static void logBarOverlapResultsSeen(
            boolean wasPanelSeen, boolean wasTap, boolean wasBarOverlap) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchBarOverlapSeen",
                getBarOverlapEnum(wasBarOverlap, wasPanelSeen, wasTap),
                BAR_OVERLAP_RESULTS_BOUNDARY);
    }

    /**
     * Log whether the UX was suppressed due to Bar overlap.
     * @param wasSuppressed Whether showing the UX was suppressed.
     */
    public static void logBarOverlapSuppression(boolean wasSuppressed) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchBarOverlap",
                wasSuppressed ? TAP_SUPPRESSED : NOT_TAP_SUPPRESSED, TAP_SUPPRESSED_BOUNDARY);
    }

    /**
     * Logs the location of a Tap and whether the panel was seen and the type of the
     * trigger.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture was a Tap or not.
     * @param triggerLocationDps The trigger location from the top of the screen.
     */
    public static void logScreenTopTapLocation(
            boolean wasPanelSeen, boolean wasTap, int triggerLocationDps) {
        // We only log Tap locations for the screen top.
        if (!wasTap) return;
        String histogram = wasPanelSeen ? "Search.ContextualSearchTopLocationSeen"
                                        : "Search.ContextualSearchTopLocationNotSeen";
        int min = 1;
        int max = 250;
        int numBuckets = 50;
        RecordHistogram.recordCustomCountHistogram(
                histogram, triggerLocationDps, min, max, numBuckets);
    }

    /**
     * Log whether the UX was suppressed due to a Tap too close to the screen top.
     * @param wasSuppressed Whether showing the UX was suppressed.
     */
    public static void logScreenTopTapSuppression(boolean wasSuppressed) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchScreenTopSuppressed",
                wasSuppressed ? TAP_SUPPRESSED : NOT_TAP_SUPPRESSED, TAP_SUPPRESSED_BOUNDARY);
    }

    /**
     * Log whether results were seen due to a Tap with broad signals.
     * @param wasSearchContentViewSeen If the panel was opened.
     * @param isSecondTap Whether this was the second tap after an initial suppressed tap.
     */
    public static void logTapSuppressionResultsSeen(
            boolean wasSearchContentViewSeen, boolean isSecondTap) {
        if (isSecondTap) {
            RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchSecondTapSeen",
                    wasSearchContentViewSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN,
                    RESULTS_SEEN_BOUNDARY);
        } else {
            RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchTapSuppressionSeen",
                    wasSearchContentViewSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN,
                    RESULTS_SEEN_BOUNDARY);
        }
    }

    /**
     * Logs whether results were seen when the selected text consisted of all capital letters.
     * @param wasSearchContentViewSeen If the panel was opened.
     */
    public static void logAllCapsResultsSeen(boolean wasSearchContentViewSeen) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchAllCapsResultsSeen",
                wasSearchContentViewSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN,
                RESULTS_SEEN_BOUNDARY);
    }

    /**
     * Logs whether results were seen when the selected text started with a capital letter but was
     * not all capital letters.
     * @param wasSearchContentViewSeen If the panel was opened.
     */
    public static void logStartedWithCapitalResultsSeen(boolean wasSearchContentViewSeen) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchStartedWithCapitalResultsSeen",
                wasSearchContentViewSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN,
                RESULTS_SEEN_BOUNDARY);
    }

    /**
     * Logs whether results were seen and whether any tap suppression heuristics were satisfied.
     * @param wasSearchContentViewSeen If the panel was opened.
     * @param wasAnySuppressionHeuristicSatisfied Whether any of the implemented suppression
     *                                            heuristics were satisfied.
     */
    public static void logAnyTapSuppressionHeuristicSatisfied(boolean wasSearchContentViewSeen,
            boolean wasAnySuppressionHeuristicSatisfied) {
        int code;
        if (wasAnySuppressionHeuristicSatisfied) {
            code = wasSearchContentViewSeen ? RESULTS_SEEN_SUPPRESSION_HEURSTIC_SATISFIED
                    : RESULTS_NOT_SEEN_SUPPRESSION_HEURSTIC_SATISFIED;
        } else {
            code = wasSearchContentViewSeen ? RESULTS_SEEN_SUPPRESSION_HEURSTIC_NOT_SATISFIED
                    : RESULTS_NOT_SEEN_SUPPRESSION_HEURSTIC_NOT_SATISFIED;
        }

        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchTapSuppressionSeen.AnyHeuristicSatisfied",
                code,
                RESULTS_SEEN_SUPPRESSION_BOUNDARY);
    }

    /**
     * Logs whether search results were seen, whether the search provider icon sprite was animated
     * when the panel first appeared, and the triggering gesture.
     * @param wasIconSpriteAnimated Whether the search provider icon sprite was animated when the
     *                              the panel first appeared.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     */
    public static void logIconSpriteAnimated(boolean wasIconSpriteAnimated, boolean wasPanelSeen,
            boolean wasTap) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchIconSpriteAnimated",
                ICON_SPRITE_ANIMATION_CODES.get(new IconSpriteAnimationKey(wasIconSpriteAnimated,
                        wasPanelSeen, wasTap)),
                ICON_SPRITE_BOUNDARY);
    }

    /**
     * Logs whether a selection is valid.
     * @param isSelectionValid Whether the selection is valid.
     */
    public static void logSelectionIsValid(boolean isSelectionValid) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchSelectionValid",
                isSelectionValid ? SELECTION_VALID : SELECTION_INVALID, SELECTION_BOUNDARY);
    }

    /**
     * Logs whether a normal priority search request failed.
     * @param isFailure Whether the request failed.
     */
    public static void logNormalPrioritySearchRequestOutcome(boolean isFailure) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchNormalPrioritySearchRequestStatus",
                isFailure ? REQUEST_FAILED : REQUEST_NOT_FAILED, REQUEST_BOUNDARY);
    }

    /**
     * Logs whether a low priority search request failed.
     * @param isFailure Whether the request failed.
     */
    public static void logLowPrioritySearchRequestOutcome(boolean isFailure) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchLowPrioritySearchRequestStatus",
                isFailure ? REQUEST_FAILED : REQUEST_NOT_FAILED, REQUEST_BOUNDARY);
    }

    /**
     * Logs whether a fallback search request failed.
     * @param isFailure Whether the request failed.
     */
    public static void logFallbackSearchRequestOutcome(boolean isFailure) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchFallbackSearchRequestStatus",
                isFailure ? REQUEST_FAILED : REQUEST_NOT_FAILED, REQUEST_BOUNDARY);
    }

    /**
     * Logs whether the SERP was fully loaded when an opened panel was closed.
     * @param fullyLoaded Whether the SERP had finished loading before the panel was closed.
     */
    public static void logSerpLoadedOnClose(boolean fullyLoaded) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchSerpLoadedOnClose",
                fullyLoaded ? FULLY_LOADED : PARTIALLY_LOADED, LOADED_BOUNDARY);
    }

    /**
     * Logs the duration since a recent scroll.
     * @param durationSinceRecentScrollMs The amount of time since the most recent scroll.
     * @param wasSearchContentViewSeen If the panel was opened.
     */
    public static void logRecentScrollDuration(
            int durationSinceRecentScrollMs, boolean wasSearchContentViewSeen) {
        String histogram = wasSearchContentViewSeen ? "Search.ContextualSearchRecentScrollSeen"
                                                    : "Search.ContextualSearchRecentScrollNotSeen";
        if (durationSinceRecentScrollMs < 1000) {
            RecordHistogram.recordCount1000Histogram(histogram, durationSinceRecentScrollMs);
        }
    }

    /**
     * Log whether the UX was suppressed by a recent scroll.
     * @param wasSuppressed Whether showing the UX was suppressed by a recent scroll.
     */
    public static void logRecentScrollSuppression(boolean wasSuppressed) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchRecentScrollSuppression",
                wasSuppressed ? TAP_SUPPRESSED : NOT_TAP_SUPPRESSED, TAP_SUPPRESSED_BOUNDARY);
    }

    /**
     * Logs the duration between the panel being triggered due to a tap or long-press and the
     * panel being dismissed due to a scroll.
     * @param durationSincePanelTriggerMs The amount of time between the panel getting triggered and
     *                                    the panel being dismissed due to a scroll.
     * @param wasSearchContentViewSeen If the panel was opened.
     */
    public static void logDurationBetweenTriggerAndScroll(
            long durationSincePanelTriggerMs, boolean wasSearchContentViewSeen) {
        String histogram = wasSearchContentViewSeen
                ? "Search.ContextualSearchDurationBetweenTriggerAndScrollSeen"
                : "Search.ContextualSearchDurationBetweenTriggerAndScrollNotSeen";
        if (durationSincePanelTriggerMs < 2000) {
            RecordHistogram.recordCustomCountHistogram(
                    histogram, (int) durationSincePanelTriggerMs, 1, 2000, 200);
        }
    }

    /**
     * Logs whether a Quick Answer caption was activated, and whether it was an answer (as opposed
     * to just being informative), and whether the panel was opened anyway.
     * Logged only for Tap events.
     * @param didActivate If the Quick Answer caption was shown.
     * @param didAnswer If the caption was considered an answer (reducing the need to open the
     *        panel).
     * @param wasSearchContentViewSeen If the panel was opened.
     */
    static void logQuickAnswerSeen(
            boolean wasSearchContentViewSeen, boolean didActivate, boolean didAnswer) {
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchQuickAnswerSeen",
                getQuickAnswerSeenValue(didActivate, didAnswer, wasSearchContentViewSeen),
                QUICK_ANSWER_SEEN_BOUNDARY);
    }

    /**
     * Logs how a state was entered for the first time within a Contextual Search.
     * @param fromState The state to transition from.
     * @param toState The state to transition to.
     * @param reason The reason for the state transition.
     */
    public static void logFirstStateEntry(PanelState fromState, PanelState toState,
            StateChangeReason reason) {
        int code;
        switch (toState) {
            case CLOSED:
                code = getStateChangeCode(fromState, reason,
                        ENTER_CLOSED_STATE_CHANGE_CODES, ENTER_CLOSED_FROM_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchEnterClosed",
                        code, ENTER_CLOSED_FROM_BOUNDARY);
                break;
            case PEEKED:
                code = getStateChangeCode(fromState, reason,
                        ENTER_PEEKED_STATE_CHANGE_CODES, ENTER_PEEKED_FROM_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchEnterPeeked",
                        code, ENTER_PEEKED_FROM_BOUNDARY);
                break;
            case EXPANDED:
                code = getStateChangeCode(fromState, reason,
                        ENTER_EXPANDED_STATE_CHANGE_CODES, ENTER_EXPANDED_FROM_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchEnterExpanded",
                        code, ENTER_EXPANDED_FROM_BOUNDARY);
                break;
            case MAXIMIZED:
                code = getStateChangeCode(fromState, reason,
                        ENTER_MAXIMIZED_STATE_CHANGE_CODES, ENTER_MAXIMIZED_FROM_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchEnterMaximized",
                        code, ENTER_MAXIMIZED_FROM_BOUNDARY);
                break;
            default:
                break;
        }
    }

    /**
     * Logs a user action for a change to the Panel state, which allows sequencing of actions.
     * @param toState The state to transition to.
     * @param reason The reason for the state transition.
     */
    public static void logPanelStateUserAction(PanelState toState, StateChangeReason reason) {
        switch (toState) {
            case CLOSED:
                if (reason == StateChangeReason.BACK_PRESS) {
                    RecordUserAction.record("ContextualSearch.BackPressClose");
                } else if (reason == StateChangeReason.CLOSE_BUTTON) {
                    RecordUserAction.record("ContextualSearch.CloseButtonClose");
                } else if (reason == StateChangeReason.SWIPE || reason == StateChangeReason.FLING) {
                    RecordUserAction.record("ContextualSearch.SwipeOrFlingClose");
                } else if (reason == StateChangeReason.TAB_PROMOTION) {
                    RecordUserAction.record("ContextualSearch.TabPromotionClose");
                } else if (reason == StateChangeReason.BASE_PAGE_TAP) {
                    RecordUserAction.record("ContextualSearch.BasePageTapClose");
                } else if (reason == StateChangeReason.BASE_PAGE_SCROLL) {
                    RecordUserAction.record("ContextualSearch.BasePageScrollClose");
                } else if (reason == StateChangeReason.SEARCH_BAR_TAP) {
                    RecordUserAction.record("ContextualSearch.SearchBarTapClose");
                } else if (reason == StateChangeReason.SERP_NAVIGATION) {
                    RecordUserAction.record("ContextualSearch.NavigationClose");
                } else {
                    RecordUserAction.record("ContextualSearch.UncommonClose");
                }
                break;
            case PEEKED:
                if (reason == StateChangeReason.TEXT_SELECT_TAP) {
                    RecordUserAction.record("ContextualSearch.TapPeek");
                } else if (reason == StateChangeReason.SWIPE || reason == StateChangeReason.FLING) {
                    RecordUserAction.record("ContextualSearch.SwipeOrFlingPeek");
                } else if (reason == StateChangeReason.TEXT_SELECT_LONG_PRESS) {
                    RecordUserAction.record("ContextualSearch.LongpressPeek");
                }
                break;
            case EXPANDED:
                if (reason == StateChangeReason.SWIPE || reason == StateChangeReason.FLING) {
                    RecordUserAction.record("ContextualSearch.SwipeOrFlingExpand");
                } else if (reason == StateChangeReason.SEARCH_BAR_TAP) {
                    RecordUserAction.record("ContextualSearch.SearchBarTapExpand");
                }
                break;
            case MAXIMIZED:
                if (reason == StateChangeReason.SWIPE || reason == StateChangeReason.FLING) {
                    RecordUserAction.record("ContextualSearch.SwipeOrFlingMaximize");
                } else if (reason == StateChangeReason.SERP_NAVIGATION) {
                    RecordUserAction.record("ContextualSearch.NavigationMaximize");
                }
                break;
            default:
                break;
        }
    }

    /**
     * Logs how a state was exited for the first time within a Contextual Search.
     * @param fromState The state to transition from.
     * @param toState The state to transition to.
     * @param reason The reason for the state transition.
     */
    public static void logFirstStateExit(PanelState fromState, PanelState toState,
            StateChangeReason reason) {
        int code;
        switch (fromState) {
            case UNDEFINED:
            case CLOSED:
                code = getStateChangeCode(toState, reason,
                        EXIT_CLOSED_TO_STATE_CHANGE_CODES, EXIT_CLOSED_TO_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchExitClosed", code, EXIT_CLOSED_TO_BOUNDARY);
                break;
            case PEEKED:
                code = getStateChangeCode(toState, reason,
                        EXIT_PEEKED_TO_STATE_CHANGE_CODES, EXIT_PEEKED_TO_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchExitPeeked", code, EXIT_PEEKED_TO_BOUNDARY);
                break;
            case EXPANDED:
                code = getStateChangeCode(toState, reason,
                        EXIT_EXPANDED_TO_STATE_CHANGE_CODES, EXIT_EXPANDED_TO_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchExitExpanded", code, EXIT_EXPANDED_TO_BOUNDARY);
                break;
            case MAXIMIZED:
                code = getStateChangeCode(toState, reason,
                        EXIT_MAXIMIZED_TO_STATE_CHANGE_CODES, EXIT_MAXIMIZED_TO_OTHER);
                RecordHistogram.recordEnumeratedHistogram(
                        "Search.ContextualSearchExitMaximized", code, EXIT_MAXIMIZED_TO_BOUNDARY);
                break;
            default:
                break;
        }
    }

    /**
     * Logs the number of impressions and CTR for the previous week for the current user.
     * @param previousWeekImpressions The number of times the user saw the Contextual Search Bar.
     * @param previousWeekCtr The CTR expressed as a percentage.
     */
    public static void logPreviousWeekCtr(int previousWeekImpressions, int previousWeekCtr) {
        RecordHistogram.recordCountHistogram(
                "Search.ContextualSearchPreviousWeekImpressions", previousWeekImpressions);
        RecordHistogram.recordPercentageHistogram(
                "Search.ContextualSearchPreviousWeekCtr", previousWeekCtr);
    }

    /**
     * Logs the number of impressions and CTR for previous 28-day period for the current user.
     * @param previous28DayImpressions The number of times the user saw the Contextual Search Bar.
     * @param previous28DayCtr The CTR expressed as a percentage.
     */
    public static void logPrevious28DayCtr(int previous28DayImpressions, int previous28DayCtr) {
        RecordHistogram.recordCountHistogram(
                "Search.ContextualSearchPrevious28DayImpressions", previous28DayImpressions);
        RecordHistogram.recordPercentageHistogram(
                "Search.ContextualSearchPrevious28DayCtr", previous28DayCtr);
    }

    /**
     * Get the encoded value to use for the Bar Overlap histogram by encoding all the input
     * parameters.
     * @param didBarOverlap Whether the selection overlapped the Bar position.
     * @param wasPanelSeen Whether the panel content was seen.
     * @param wasTap Whether the gesture was a Tap.
     * @return The value for the enum histogram.
     */
    private static int getBarOverlapEnum(
            boolean didBarOverlap, boolean wasPanelSeen, boolean wasTap) {
        if (wasTap) {
            if (didBarOverlap) {
                if (wasPanelSeen) {
                    return BAR_OVERLAP_RESULTS_SEEN_FROM_TAP;
                } else {
                    return BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_TAP;
                }
            } else {
                if (wasPanelSeen) {
                    return NO_BAR_OVERLAP_RESULTS_SEEN_FROM_TAP;
                } else {
                    return NO_BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_TAP;
                }
            }
        } else {
            if (didBarOverlap) {
                if (wasPanelSeen) {
                    return BAR_OVERLAP_RESULTS_SEEN_FROM_LONG_PRESS;
                } else {
                    return BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_LONG_PRESS;
                }
            } else {
                if (wasPanelSeen) {
                    return NO_BAR_OVERLAP_RESULTS_SEEN_FROM_LONG_PRESS;
                } else {
                    return NO_BAR_OVERLAP_RESULTS_NOT_SEEN_FROM_LONG_PRESS;
                }
            }
        }
    }

    /**
     * Logs that the conditions are right to force the translation one-box, and whether it
     * was actually forced or not.
     * @param didForceTranslate Whether the translation onebox was forced.
     */
    public static void logTranslateOnebox(boolean didForceTranslate) {
        int code = didForceTranslate ? DID_FORCE_TRANSLATE : WOULD_FORCE_TRANSLATE;
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchShouldTranslate", code, FORCE_TRANSLATE_BOUNDARY);
    }

    /**
     * Logs whether a certain category of a blacklisted term resulted in the search results
     * being seen.
     * @param reason The given reason.
     * @param wasSeen Whether the search results were seen.
     */
    public static void logBlacklistSeen(BlacklistReason reason, boolean wasSeen) {
        if (reason == null) reason = BlacklistReason.NONE;
        int code = ContextualSearchBlacklist.getBlacklistMetricsCode(reason, wasSeen);
        RecordHistogram.recordEnumeratedHistogram("Search.ContextualSearchBlacklistSeen",
                code, ContextualSearchBlacklist.BLACKLIST_BOUNDARY);
    }

    /**
     * Logs whether Contextual Cards data was shown. Should be logged on tap if Contextual
     * Cards integration is enabled.
     * @param shown Whether Contextual Cards data was shown in the Bar.
     */
    public static void logContextualCardsDataShown(boolean shown) {
        RecordHistogram.recordBooleanHistogram(
                "Search.ContextualSearchContextualCardsIntegration.DataShown", shown);
    }

    /**
     * Logs whether results were seen when Contextual Cards data was shown.
     * @param wasSeen Whether the search results were seen.
     */
    public static void logContextualCardsResultsSeen(boolean wasSeen) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchContextualCardsIntegration.ResultsSeen",
                wasSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN, RESULTS_SEEN_BOUNDARY);
    }

    /**
     * Logs whether a quick action intent resolved to zero, one, or many apps.
     * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
     * @param numMatchingAppsApps The number of apps that the resolved intent matched.
     */
    public static void logQuickActionIntentResolution(int quickActionCategory,
            int numMatchingAppsApps) {
        int code = numMatchingAppsApps == 0 ? QUICK_ACTION_RESOLVE_FAILED
                : numMatchingAppsApps == 1 ? QUICK_ACTION_RESOLVE_SINGLE
                        : QUICK_ACTION_RESOLVE_MULTIPLE;
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchQuickActions.IntentResolution."
                        + getLabelForQuickActionCategory(quickActionCategory),
                code, QUICK_ACTION_RESOLVE_BOUNDARY);
    }

    /**
     * Logs whether a quick action was shown, and the quick aciton category if a quick action was
     * shown. Should be logged on tap if Contextual Search single actions are enabled.
     * @param quickActionShown Whether a quick action was shown.
     * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
     */
    public static void logQuickActionShown(boolean quickActionShown, int quickActionCategory) {
        RecordHistogram.recordBooleanHistogram(
                "Search.ContextualSearchQuickActions.Shown", quickActionShown);
        if (quickActionShown) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Search.ContextualSearchQuickActions.Category",
                    quickActionCategory, QuickActionCategory.BOUNDARY);
        }
    }

    /**
     * Logs whether results were seen when a quick action was present.
     * @param wasSeen Whether the search results were seen.
     * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
     */
    public static void logQuickActionResultsSeen(boolean wasSeen, int quickActionCategory) {
        RecordHistogram.recordEnumeratedHistogram(
                "Search.ContextualSearchQuickActions.ResultsSeen."
                        + getLabelForQuickActionCategory(quickActionCategory),
                wasSeen ? RESULTS_SEEN : RESULTS_NOT_SEEN, RESULTS_SEEN_BOUNDARY);
    }

    /**
     * Logs whether a quick action was clicked.
     * @param wasClicked Whether the quick action was clicked
     * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
     */
    public static void logQuickActionClicked(boolean wasClicked, int quickActionCategory) {
        RecordHistogram.recordBooleanHistogram(
                "Search.ContextualSearchQuickActions.Clicked."
                        + getLabelForQuickActionCategory(quickActionCategory),
                 wasClicked);
    }

    /**
     * Gets the state-change code for the given parameters by doing a lookup in the given map.
     * @param state The panel state.
     * @param reason The reason the state changed.
     * @param stateChangeCodes The map of state and reason to code.
     * @param defaultCode The code to return if the given values are not found in the map.
     * @return The code to write into an enum histogram, based on the given map.
     */
    private static int getStateChangeCode(PanelState state, StateChangeReason reason,
            Map<StateChangeKey, Integer> stateChangeCodes, int defaultCode) {
        Integer code = stateChangeCodes.get(new StateChangeKey(state, reason));
        if (code != null) {
            return code;
        }
        return defaultCode;
    }

    /**
     * Gets the panel-seen code for the given parameters by doing a lookup in the seen-by-gesture
     * map.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     * @return The code to write into a panel-seen histogram.
     */
    private static int getPanelSeenByGestureStateCode(boolean wasPanelSeen, boolean wasTap) {
        return SEEN_BY_GESTURE_CODES.get(new Pair<Boolean, Boolean>(wasPanelSeen, wasTap));
    }

    /**
     * Gets the promo-outcome code for the given parameter by doing a lookup in the
     * promo-by-gesture map.
     * @param preferenceValue The code for the current preference value.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     * @return The code to write into a promo-outcome histogram.
     */
    private static int getPromoByGestureStateCode(int preferenceValue, boolean wasTap) {
        return PROMO_BY_GESTURE_CODES.get(new Pair<Integer, Boolean>(preferenceValue, wasTap));
    }

    /**
     * @return The code for the Contextual Search preference.
     */
    private static int getPreferenceValue() {
        PrefServiceBridge preferences = PrefServiceBridge.getInstance();
        if (preferences.isContextualSearchUninitialized()) {
            return PREFERENCE_UNINITIALIZED;
        } else if (preferences.isContextualSearchDisabled()) {
            return PREFERENCE_DISABLED;
        }
        return PREFERENCE_ENABLED;
    }

    /**
     * Gets the encode value for quick answers seen.
     * @param didActivate Whether the quick answer was shown.
     * @param didAnswer Whether the caption was a full answer, not just a hint.
     * @param wasSeen Whether the search panel was opened.
     * @return The encoded value.
     */
    private static int getQuickAnswerSeenValue(
            boolean didActivate, boolean didAnswer, boolean wasSeen) {
        if (wasSeen) {
            if (didActivate) {
                if (didAnswer) {
                    return QUICK_ANSWER_ACTIVATED_WAS_AN_ANSWER_SEEN;
                } else {
                    return QUICK_ANSWER_ACTIVATED_NOT_AN_ANSWER_SEEN;
                }
            } else {
                return QUICK_ANSWER_NOT_ACTIVATED_SEEN;
            }
        } else {
            if (didActivate) {
                if (didAnswer) {
                    return QUICK_ANSWER_ACTIVATED_WAS_AN_ANSWER_NOT_SEEN;
                } else {
                    return QUICK_ANSWER_ACTIVATED_NOT_AN_ANSWER_NOT_SEEN;
                }
            } else {
                return QUICK_ANSWER_NOT_ACTIVATED_NOT_SEEN;
            }
        }
    }

    /**
     * Logs to a seen-by-gesture histogram of the given name.
     * @param wasPanelSeen Whether the panel was seen.
     * @param wasTap Whether the gesture that originally caused the panel to show was a Tap.
     * @param histogramName The full name of the histogram to log to.
     */
    private static void logHistogramByGesture(boolean wasPanelSeen, boolean wasTap,
            String histogramName) {
        RecordHistogram.recordEnumeratedHistogram(histogramName,
                getPanelSeenByGestureStateCode(wasPanelSeen, wasTap),
                RESULTS_BY_GESTURE_BOUNDARY);
    }

    private static String getLabelForQuickActionCategory(int quickActionCategory) {
        switch(quickActionCategory) {
            case QuickActionCategory.ADDRESS:
                return "Address";
            case QuickActionCategory.EMAIL:
                return "Email";
            case QuickActionCategory.EVENT:
                return "Event";
            case QuickActionCategory.PHONE:
                return "Phone";
            default:
                return "None";
        }
    }
}