chromium/chrome/browser/optimization_guide/android/java/src/org/chromium/chrome/browser/optimization_guide/OptimizationGuideBridge.java

// Copyright 2020 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.optimization_guide;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.ThreadUtils;
import org.chromium.components.optimization_guide.OptimizationGuideDecision;
import org.chromium.components.optimization_guide.proto.CommonTypesProto.Any;
import org.chromium.components.optimization_guide.proto.CommonTypesProto.RequestContext;
import org.chromium.components.optimization_guide.proto.HintsProto.OptimizationType;
import org.chromium.components.optimization_guide.proto.HintsProto.RequestContextMetadata;
import org.chromium.components.optimization_guide.proto.PushNotificationProto.HintNotificationPayload;
import org.chromium.url.GURL;

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

/**
 * Provides access to the optimization guide using the C++ OptimizationGuideKeyedService.
 *
 * <p>An instance of this class must be created, used, and destroyed on the UI thread.
 */
@JNINamespace("optimization_guide::android")
public class OptimizationGuideBridge {
    private long mNativeOptimizationGuideBridge;

    /** Interface to implement to receive decisions from the optimization guide. */
    public interface OptimizationGuideCallback {
        void onOptimizationGuideDecision(
                @OptimizationGuideDecision int decision, @Nullable Any metadata);
    }

    /** Interface to implement to receive on-demand decisions from the optimization guide. */
    public interface OnDemandOptimizationGuideCallback {
        void onOnDemandOptimizationGuideDecision(
                GURL url,
                OptimizationType optimizationType,
                @OptimizationGuideDecision int decision,
                @Nullable Any metadata);
    }

    @VisibleForTesting
    @CalledByNative
    protected OptimizationGuideBridge(long nativeOptimizationGuideBridge) {
        ThreadUtils.assertOnUiThread();

        mNativeOptimizationGuideBridge = nativeOptimizationGuideBridge;
    }

    /**
     * Registers the optimization types that intend to be queried during the session. It is expected
     * for this to be called after the browser has been initialized.
     */
    public void registerOptimizationTypes(@Nullable List<OptimizationType> optimizationTypes) {
        ThreadUtils.assertOnUiThread();
        if (mNativeOptimizationGuideBridge == 0) return;

        if (optimizationTypes == null) {
            optimizationTypes = new ArrayList<>();
        }
        int[] intOptimizationTypes = new int[optimizationTypes.size()];
        for (int i = 0; i < optimizationTypes.size(); i++) {
            intOptimizationTypes[i] = optimizationTypes.get(i).getNumber();
        }

        OptimizationGuideBridgeJni.get()
                .registerOptimizationTypes(mNativeOptimizationGuideBridge, intOptimizationTypes);
    }

    /**
     * Returns whether {@link optimizationType} can be applied for {@link url}. This should
     * only be called for main frame navigations or future main frame navigations.
     *
     * @param url main frame navigation URL an optimization decision is being made for.
     * @param optimizationType {@link OptimizationType} decision is being made for
     * @param callback {@link OptimizationGuideCallback} optimization decision is passed in
     */
    public void canApplyOptimization(
            GURL url, OptimizationType optimizationType, OptimizationGuideCallback callback) {
        ThreadUtils.assertOnUiThread();

        if (mNativeOptimizationGuideBridge == 0) {
            callback.onOptimizationGuideDecision(OptimizationGuideDecision.UNKNOWN, null);
            return;
        }

        OptimizationGuideBridgeJni.get()
                .canApplyOptimization(
                        mNativeOptimizationGuideBridge,
                        url,
                        optimizationType.getNumber(),
                        callback);
    }

    /**
     * Invokes {@link OnDemandOptimizationGuideCallback} with the decision for all types contained
     * in {@link optimizationTypes} for each URL contained in {@link urls}, when sufficient
     * information has been collected to make decisions. {@link requestContext} must be included to
     * indicate when the request is being made to determine the appropriate permissions to make the
     * request for accounting purposes.
     *
     * <p>It is expected for consumers to consult with the Optimization Guide team before using this
     * API. If approved, add your request context to the assertion list here.
     */
    public void canApplyOptimizationOnDemand(
            List<GURL> urls,
            List<OptimizationType> optimizationTypes,
            RequestContext requestContext,
            OnDemandOptimizationGuideCallback callback,
            RequestContextMetadata requestContextMetadata) {
        ThreadUtils.assertOnUiThread();

        assert isRequestContextAllowedForOnDemandOptimizations(requestContext);

        if (mNativeOptimizationGuideBridge == 0) {
            for (GURL url : urls) {
                for (OptimizationType optimizationType : optimizationTypes) {
                    callback.onOnDemandOptimizationGuideDecision(
                            url, optimizationType, OptimizationGuideDecision.UNKNOWN, null);
                }
            }
            return;
        }

        GURL[] gurlsArray = new GURL[urls.size()];
        urls.toArray(gurlsArray);
        int[] intOptimizationTypes = new int[optimizationTypes.size()];
        for (int i = 0; i < optimizationTypes.size(); i++) {
            intOptimizationTypes[i] = optimizationTypes.get(i).getNumber();
        }

        byte[] requestContextMetadataSerialized = requestContextMetadata.toByteArray();

        OptimizationGuideBridgeJni.get()
                .canApplyOptimizationOnDemand(
                        mNativeOptimizationGuideBridge,
                        gurlsArray,
                        intOptimizationTypes,
                        requestContext.getNumber(),
                        callback,
                        requestContextMetadataSerialized);
    }

    public void onNewPushNotification(HintNotificationPayload notification) {
        ThreadUtils.assertOnUiThread();
        if (mNativeOptimizationGuideBridge == 0) {
            OptimizationGuidePushNotificationManager.onPushNotificationNotHandledByNative(
                    notification);
            return;
        }
        OptimizationGuideBridgeJni.get()
                .onNewPushNotification(mNativeOptimizationGuideBridge, notification.toByteArray());
    }

    /**
     * Signal native OptimizationGuide that deferred startup has occurred. This enables
     * OptimizationGuide to fetch hints in the background while minimizing the risk of
     * regressing key performance metrics such as jank. This method should only be
     * called by ProcessInitializationHandler.
     */
    public void onDeferredStartup() {
        if (mNativeOptimizationGuideBridge == 0) {
            return;
        }
        OptimizationGuideBridgeJni.get().onDeferredStartup(mNativeOptimizationGuideBridge);
    }

    private boolean isRequestContextAllowedForOnDemandOptimizations(RequestContext requestContext) {
        switch (requestContext) {
            case CONTEXT_PAGE_INSIGHTS_HUB:
            case CONTEXT_NON_PERSONALIZED_PAGE_INSIGHTS_HUB:
                return true;
            default:
                return false;
        }
    }

    @CalledByNative
    private static void onOptimizationGuideDecision(
            OptimizationGuideCallback callback,
            @OptimizationGuideDecision int optimizationGuideDecision,
            @Nullable byte[] serializedAnyMetadata) {
        callback.onOptimizationGuideDecision(
                optimizationGuideDecision, deserializeAnyMetadata(serializedAnyMetadata));
    }

    @CalledByNative
    private static void onOnDemandOptimizationGuideDecision(
            OnDemandOptimizationGuideCallback callback,
            GURL url,
            int optimizationTypeInt,
            @OptimizationGuideDecision int optimizationGuideDecision,
            @Nullable byte[] serializedAnyMetadata) {
        OptimizationType optimizationType = OptimizationType.forNumber(optimizationTypeInt);
        if (optimizationType == null) return;
        callback.onOnDemandOptimizationGuideDecision(
                url,
                optimizationType,
                optimizationGuideDecision,
                deserializeAnyMetadata(serializedAnyMetadata));
    }

    /** Clears all cached push notifications for the given optimization type. */
    @CalledByNative
    private static void clearCachedPushNotifications(int optimizationTypeInt) {
        OptimizationType optimizationType = OptimizationType.forNumber(optimizationTypeInt);
        if (optimizationType == null) return;

        OptimizationGuidePushNotificationManager.clearCacheForOptimizationType(optimizationType);
    }

    /** Returns an array of all the optimization types that have cached push notifications. */
    @CalledByNative
    private static int[] getOptTypesWithPushNotifications() {
        List<OptimizationType> cachedTypes =
                OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications();
        int[] intCachedTypes = new int[cachedTypes.size()];
        for (int i = 0; i < cachedTypes.size(); i++) {
            intCachedTypes[i] = cachedTypes.get(i).getNumber();
        }
        return intCachedTypes;
    }

    /**
     * Returns an array of all the optimization types that overflowed their cache for push
     * notifications.
     */
    @CalledByNative
    private static int[] getOptTypesThatOverflowedPushNotifications() {
        List<OptimizationType> overflows =
                OptimizationGuidePushNotificationManager
                        .getOptTypesThatOverflowedPushNotifications();
        int[] intOverflows = new int[overflows.size()];
        for (int i = 0; i < overflows.size(); i++) {
            intOverflows[i] = overflows.get(i).getNumber();
        }
        return intOverflows;
    }

    /** Returns a 2D byte array of all cached push notifications for the given optimization type. */
    @CalledByNative
    private static byte[][] getEncodedPushNotifications(int optimizationTypeInt) {
        OptimizationType optimizationType = OptimizationType.forNumber(optimizationTypeInt);
        if (optimizationType == null) return null;

        HintNotificationPayload[] notifications =
                OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
                        optimizationType);
        if (notifications == null) return null;

        byte[][] encoded_notifications = new byte[notifications.length][];
        for (int i = 0; i < notifications.length; i++) {
            encoded_notifications[i] = notifications[i].toByteArray();
        }

        return encoded_notifications;
    }

    /**
     * Called when a push notification that was passed to native immediately (without having been
     * cached) is unable to be stored right now, so it should be cached.
     */
    @CalledByNative
    private static void onPushNotificationNotHandledByNative(byte[] encodedNotification) {
        HintNotificationPayload notification;
        try {
            notification = HintNotificationPayload.parseFrom(encodedNotification);
        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
            return;
        }
        OptimizationGuidePushNotificationManager.onPushNotificationNotHandledByNative(notification);
    }

    private static @Nullable Any deserializeAnyMetadata(@Nullable byte[] serializedAnyMetadata) {
        if (serializedAnyMetadata == null) {
            return null;
        }

        Any anyMetadata;
        try {
            anyMetadata = Any.parseFrom(serializedAnyMetadata);
        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
            return null;
        }
        return anyMetadata;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {
        void registerOptimizationTypes(long nativeOptimizationGuideBridge, int[] optimizationTypes);

        void canApplyOptimization(
                long nativeOptimizationGuideBridge,
                @JniType("GURL") GURL url,
                int optimizationType,
                OptimizationGuideCallback callback);

        void canApplyOptimizationOnDemand(
                long nativeOptimizationGuideBridge,
                @JniType("std::vector<GURL>") GURL[] urls,
                int[] optimizationTypes,
                int requestContext,
                OnDemandOptimizationGuideCallback callback,
                @JniType("jni_zero::ByteArrayView") byte[] requestContextMetadata);

        void onNewPushNotification(long nativeOptimizationGuideBridge, byte[] encodedNotification);

        void onDeferredStartup(long nativeOptimizationGuideBridge);
    }
}