chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/PreWarmingRecycledViewPool.java

// Copyright 2023 The Chromium Authors
// 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.omnibox.suggestions;

import android.content.Context;
import android.os.Handler;
import android.widget.FrameLayout;

import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import org.chromium.base.TraceEvent;
import org.chromium.chrome.browser.omnibox.OmniboxMetrics;
import org.chromium.components.omnibox.OmniboxFeatures;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * RecyclerView pool that:
 *
 * <ul>
 *   <li>Pre-creates a hardcoded set of ViewHolders.
 *   <li>Records the performance of the view recycling mechanism.
 * </ul>
 */
public class PreWarmingRecycledViewPool extends RecycledViewPool {
    private static final long STEP_MILLIS = 50;

    private static class ViewTypeAndCount {
        public final int viewType;
        public final int count;

        public ViewTypeAndCount(int viewType, int count) {
            this.viewType = viewType;
            assert count > 0;
            this.count = count;
        }
    }

    private final ViewTypeAndCount[] mViewsToCreate =
            new ViewTypeAndCount[] {
                new ViewTypeAndCount(OmniboxSuggestionUiType.EDIT_URL_SUGGESTION, 1),
                new ViewTypeAndCount(OmniboxSuggestionUiType.TILE_NAVSUGGEST, 1),
                new ViewTypeAndCount(OmniboxSuggestionUiType.HEADER, 1),
                new ViewTypeAndCount(OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION, 1),
                new ViewTypeAndCount(OmniboxSuggestionUiType.DEFAULT, 15),
                new ViewTypeAndCount(OmniboxSuggestionUiType.ENTITY_SUGGESTION, 3)
            };

    private OmniboxSuggestionsDropdownAdapter mAdapter;
    private final Optional<Handler> mHandler;
    private final FrameLayout mPlaceholderParent;
    private boolean mStopCreatingViews;
    private final List<ViewHolder> mPrewarmedViews = new ArrayList<>(22);

    PreWarmingRecycledViewPool(OmniboxSuggestionsDropdownAdapter adapter, Context context) {
        mAdapter = adapter;
        mHandler =
                OmniboxFeatures.sAsyncViewInflation.isEnabled()
                        // If AsyncViewInflation is enabled, we use AsyncViewStub to handle
                        // asynchrony and we
                        // don't need to do it ourselves.
                        ? Optional.empty()
                        // Otherwise, we handle asynchrony.
                        : Optional.of(new Handler());
        mPlaceholderParent = new FrameLayout(context);
        // The list below should include suggestions defined in OmniboxSuggestionUiType
        // and specify the maximum anticipated volume of suggestions of each type.
        // For readability reasons, keep the order of this list same as the order of
        // the types defined in OmniboxSuggestionUiType.
        setMaxRecycledViews(OmniboxSuggestionUiType.DEFAULT, 20);
        setMaxRecycledViews(OmniboxSuggestionUiType.EDIT_URL_SUGGESTION, 1);
        setMaxRecycledViews(OmniboxSuggestionUiType.ANSWER_SUGGESTION, 1);
        setMaxRecycledViews(OmniboxSuggestionUiType.ENTITY_SUGGESTION, 8);

        setMaxRecycledViews(OmniboxSuggestionUiType.TAIL_SUGGESTION, 15);
        setMaxRecycledViews(OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION, 1);
        setMaxRecycledViews(OmniboxSuggestionUiType.HEADER, 4);
        setMaxRecycledViews(OmniboxSuggestionUiType.TILE_NAVSUGGEST, 1);
        setMaxRecycledViews(OmniboxSuggestionUiType.GROUP_SEPARATOR, 1);
        setMaxRecycledViews(OmniboxSuggestionUiType.QUERY_TILES, 1);
    }

    public void destroy() {
        stopCreatingViews();
        mAdapter = null;
    }

    public void onNativeInitialized() {
        if (OmniboxFeatures.shouldPreWarmRecyclerViewPool()) {
            startCreatingViews();
        }
    }

    /**
     * Starts creating views. This will immediately post a separate delayed task for every view we
     * intend to create with a delay equal to STEP_MILLIS * order_of_view_creation.
     */
    public void startCreatingViews() {
        if (mStopCreatingViews) return;
        for (var viewTypeAndCount : mViewsToCreate) {
            for (int index = 0; index < viewTypeAndCount.count; ++index) {
                Runnable createViewRunnable = () -> createViewHolder(viewTypeAndCount.viewType);
                final long delay = STEP_MILLIS * (index + 1);
                mHandler.ifPresentOrElse(
                        h -> h.postDelayed(createViewRunnable, delay), createViewRunnable);
            }
        }

        // Synchronously apply all views.
        if (mHandler.isEmpty()) {
            putViewsIntoPool();
        }
    }

    /**
     * Stops the task posted by {@link #startCreatingViews()} from creating any more views and
     * removes it from the handler. Places any pre-created views into the pool.
     */
    @VisibleForTesting
    void stopCreatingViews() {
        if (mStopCreatingViews) return;
        mStopCreatingViews = true;
        mHandler.ifPresent(h -> h.removeCallbacks(null));
        putViewsIntoPool();
    }

    private void createViewHolder(@OmniboxSuggestionUiType int viewType) {
        if (mAdapter == null || mStopCreatingViews) return;
        try (TraceEvent t = TraceEvent.scoped("PreWarmingRecycledViewPool.createNextViewHolder")) {
            mPrewarmedViews.add(mAdapter.createViewHolder(mPlaceholderParent, viewType));
        }
    }

    private void putViewsIntoPool() {
        for (var viewHolder : mPrewarmedViews) {
            putRecycledView(viewHolder);
        }
        mPrewarmedViews.clear();
    }

    @Override
    public ViewHolder getRecycledView(int viewType) {
        stopCreatingViews();
        ViewHolder result = super.getRecycledView(viewType);
        if (result == null) {
            OmniboxMetrics.recordSuggestionsViewCreatedType(viewType);
        } else {
            OmniboxMetrics.recordSuggestionsViewReusedType(viewType);
        }
        return result;
    }
}