chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupFaviconCluster.java

// Copyright 2024 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.tasks.tab_management;

import android.content.Context;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;

import org.chromium.url.GURL;

import java.util.List;

/** Parent view of the up to four corner favicon images/counts. */
public class TabGroupFaviconCluster extends ConstraintLayout {

    /**
     * On the drawable side, the corner indexes go clockwise. On the tab side, we order them like
     * text, left to right and top to bottom. The bottom two corners are swapped between these two
     * schemes.
     */
    private static final int[] TAB_TO_CORNER_ORDER = {
        Corner.TOP_LEFT, Corner.TOP_RIGHT, Corner.BOTTOM_LEFT, Corner.BOTTOM_RIGHT,
    };

    /** The number of corners that callers should assume we support. */
    public static int CORNER_COUNT = TAB_TO_CORNER_ORDER.length;

    /**
     * Holds all the data needed to configure the favicon cluster. Currently no optimizations to
     * help update a single corner at a time. Instead everything must be set. But this approach
     * simplifies caller's work.
     */
    public static class ClusterData {
        public final FaviconResolver faviconResolver;
        public final int totalCount;
        public final List<GURL> firstUrls;

        public ClusterData(FaviconResolver faviconResolver, int totalCount, List<GURL> firstUrls) {
            this.faviconResolver = faviconResolver;
            this.totalCount = totalCount;

            // Verify our caller is doing reasonable things. Doesn't really matter if they pass 3 or
            // 4 urls for high total counts.
            if (totalCount < CORNER_COUNT) {
                assert firstUrls.size() == totalCount;
            } else {
                assert firstUrls.size() <= CORNER_COUNT;
            }

            this.firstUrls = firstUrls;
        }
    }

    /** Constructor for inflation. */
    public TabGroupFaviconCluster(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        for (int corner = Corner.TOP_LEFT; corner <= Corner.BOTTOM_LEFT; corner++) {
            TabGroupFaviconQuarter quarter = getTabGroupFaviconQuarter(corner);
            quarter.adjustPositionForCorner(corner, getId());
        }
    }

    void updateCornersForClusterData(ClusterData clusterData) {
        for (int i = 0; i < CORNER_COUNT; i++) {
            @Corner int corner = TAB_TO_CORNER_ORDER[i];
            TabGroupFaviconQuarter quarter = getTabGroupFaviconQuarter(corner);
            if (corner == Corner.BOTTOM_RIGHT && clusterData.totalCount > CORNER_COUNT) {
                // Add one because the plus count occludes one favicon.
                quarter.setPlusCount(clusterData.totalCount - CORNER_COUNT + 1);
            } else if (clusterData.firstUrls.size() > i) {
                GURL url = clusterData.firstUrls.get(i);
                clusterData.faviconResolver.resolve(url, quarter::setImage);
            } else {
                quarter.clear();
            }
        }
    }

    private TabGroupFaviconQuarter getTabGroupFaviconQuarter(@Corner int corner) {
        return (TabGroupFaviconQuarter) getChildAt(corner);
    }
}