chromium/content/public/android/java/src/org/chromium/content/browser/AttributionOsLevelManager.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.content.browser;

import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.LimitExceededException;
import android.os.Process;
import android.view.MotionEvent;

import androidx.annotation.IntDef;
import androidx.annotation.OptIn;
import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures;
import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest;
import androidx.privacysandbox.ads.adservices.measurement.SourceRegistrationRequest;
import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams;
import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest;
import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams;
import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest;

import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;

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

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.url.GURL;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeoutException;

/**
 * Handles passing registrations with Web Attribution Reporting API to the underlying native
 * library.
 */
@JNINamespace("content")
public class AttributionOsLevelManager {
    private static final String TAG = "AttributionManager";
    // TODO: replace with constant in android.Manifest.permission once it becomes available in U.
    private static final String PERMISSION_ACCESS_ADSERVICES_ATTRIBUTION =
            "android.permission.ACCESS_ADSERVICES_ATTRIBUTION";

    // Used for testing
    private static MeasurementManagerFutures sManagerForTesting;

    private long mNativePtr;
    private MeasurementManagerFutures mManager;

    @IntDef({
        OperationType.REGISTER_SOURCE,
        OperationType.REGISTER_WEB_SOURCE,
        OperationType.REGISTER_TRIGGER,
        OperationType.REGISTER_WEB_TRIGGER,
        OperationType.GET_MEASUREMENT_API_STATUS,
        OperationType.DELETE_REGISTRATIONS
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface OperationType {
        int REGISTER_SOURCE = 0;
        int REGISTER_WEB_SOURCE = 1;
        int REGISTER_TRIGGER = 2;
        int REGISTER_WEB_TRIGGER = 3;
        int GET_MEASUREMENT_API_STATUS = 4;
        int DELETE_REGISTRATIONS = 5;
    }

    // These values are persisted to logs. Entries should not be renumbered and
    // numeric values should never be reused.
    @IntDef({
        OperationResult.SUCCESS,
        OperationResult.ERROR_UNKNOWN,
        OperationResult.ERROR_ILLEGAL_ARGUMENT,
        OperationResult.ERROR_IO,
        OperationResult.ERROR_ILLEGAL_STATE,
        OperationResult.ERROR_SECURITY,
        OperationResult.ERROR_TIMEOUT,
        OperationResult.ERROR_LIMIT_EXCEEDED,
        OperationResult.ERROR_INTERNAL,
        OperationResult.ERROR_BACKGROUND_CALLER,
        OperationResult.ERROR_VERSION_UNSUPPORTED,
        OperationResult.ERROR_PERMISSION_UNGRANTED,
        OperationResult.ERROR_SERVICE_UNAVAILABLE,
        OperationResult.ERROR_API_RATE_LIMIT_EXCEEDED,
        OperationResult.ERROR_SERVER_RATE_LIMIT_EXCEEDED,
        OperationResult.ERROR_CALLER_NOT_ALLOWED_TO_CROSS_USER_BOUNDARIES,
        OperationResult.ERROR_CALLER_NOT_ALLOWED_ON_BEHALF,
        OperationResult.ERROR_PERMISSION_NOT_REQUESTED,
        OperationResult.ERROR_CALLER_NOT_ALLOWED,
        OperationResult.ERROR_ENCRYPTION_FAILURE,
        OperationResult.ERROR_SERVICE_NOT_FOUND,
        OperationResult.ERROR_INVALID_OBJECT,
        OperationResult.COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface OperationResult {
        int SUCCESS = 0;
        int ERROR_UNKNOWN = 1;
        int ERROR_ILLEGAL_ARGUMENT = 2;
        int ERROR_IO = 3;
        int ERROR_ILLEGAL_STATE = 4;
        int ERROR_SECURITY = 5;
        int ERROR_TIMEOUT = 6;
        int ERROR_LIMIT_EXCEEDED = 7;
        int ERROR_INTERNAL = 8;
        int ERROR_BACKGROUND_CALLER = 9;
        int ERROR_VERSION_UNSUPPORTED = 10;
        int ERROR_PERMISSION_UNGRANTED = 11;
        int ERROR_SERVICE_UNAVAILABLE = 12;
        int ERROR_API_RATE_LIMIT_EXCEEDED = 13;
        int ERROR_SERVER_RATE_LIMIT_EXCEEDED = 14;
        int ERROR_CALLER_NOT_ALLOWED_TO_CROSS_USER_BOUNDARIES = 15;
        int ERROR_CALLER_NOT_ALLOWED_ON_BEHALF = 16;
        int ERROR_PERMISSION_NOT_REQUESTED = 17;
        int ERROR_CALLER_NOT_ALLOWED = 18;
        int ERROR_ENCRYPTION_FAILURE = 19;
        int ERROR_SERVICE_NOT_FOUND = 20;
        int ERROR_INVALID_OBJECT = 21;
        int COUNT = 22;
    }

    private static boolean supportsAttribution() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
    }

    private static @OperationResult int getOperationResultFromMessage(String message) {
        if (message == null) {
            return OperationResult.ERROR_UNKNOWN;
        } else {
            String lowerMessage = message.toLowerCase(Locale.US);
            if (lowerMessage.contains("background")) {
                return OperationResult.ERROR_BACKGROUND_CALLER;
            } else if (lowerMessage.contains("unable to find the service")) {
                return OperationResult.ERROR_SERVICE_NOT_FOUND;
            } else if (lowerMessage.contains("service is not available")) {
                return OperationResult.ERROR_SERVICE_UNAVAILABLE;
            } else if (lowerMessage.contains("api rate limit exceeded")) {
                return OperationResult.ERROR_API_RATE_LIMIT_EXCEEDED;
            } else if (lowerMessage.contains("server rate limit exceeded")) {
                return OperationResult.ERROR_SERVER_RATE_LIMIT_EXCEEDED;
            } else if (lowerMessage.contains(
                    "caller is not authorized to access information from another user")) {
                return OperationResult.ERROR_CALLER_NOT_ALLOWED_TO_CROSS_USER_BOUNDARIES;
            } else if (lowerMessage.contains(
                    "caller is not allowed to perform this operation on behalf of the given"
                            + " package")) {
                return OperationResult.ERROR_CALLER_NOT_ALLOWED_ON_BEHALF;
            } else if (lowerMessage.contains("permission was not requested")) {
                return OperationResult.ERROR_PERMISSION_NOT_REQUESTED;
            } else if (lowerMessage.contains("caller is not allowed")) {
                return OperationResult.ERROR_CALLER_NOT_ALLOWED;
            } else if (lowerMessage.contains("api time out")) {
                return OperationResult.ERROR_TIMEOUT;
            } else if (lowerMessage.contains("failed to encrypt responses")) {
                return OperationResult.ERROR_ENCRYPTION_FAILURE;
            } else if (lowerMessage.contains(
                    "service received an invalid object from the server")) {
                return OperationResult.ERROR_INVALID_OBJECT;
            } else {
                return OperationResult.ERROR_UNKNOWN;
            }
        }
    }

    private static @OperationResult int convertToOperationResult(Throwable thrown) {
        @OperationResult int result = getOperationResultFromMessage(thrown.getMessage());
        if (result != OperationResult.ERROR_UNKNOWN) {
            return result;
        } else if (thrown instanceof IllegalArgumentException) {
            return OperationResult.ERROR_ILLEGAL_ARGUMENT;
        } else if (thrown instanceof IOException) {
            return OperationResult.ERROR_IO;
        } else if (thrown instanceof IllegalStateException) {
            return OperationResult.ERROR_ILLEGAL_STATE;
        } else if (thrown instanceof SecurityException) {
            return OperationResult.ERROR_SECURITY;
        } else if (thrown instanceof TimeoutException) {
            return OperationResult.ERROR_TIMEOUT;
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                && thrown instanceof LimitExceededException) {
            return OperationResult.ERROR_LIMIT_EXCEEDED;
        } else if (thrown instanceof InvalidObjectException) {
            return OperationResult.ERROR_INVALID_OBJECT;
        } else {
            return OperationResult.ERROR_UNKNOWN;
        }
    }

    private static void recordOperationResult(
            @OperationType int type, @OperationResult int result) {
        String suffix = "";
        switch (type) {
            case OperationType.REGISTER_SOURCE:
                suffix = "RegisterSource";
                break;
            case OperationType.REGISTER_WEB_SOURCE:
                suffix = "RegisterWebSource";
                break;
            case OperationType.REGISTER_TRIGGER:
                suffix = "RegisterTrigger";
                break;
            case OperationType.REGISTER_WEB_TRIGGER:
                suffix = "RegisterWebTrigger";
                break;
            case OperationType.GET_MEASUREMENT_API_STATUS:
                suffix = "GetMeasurementApiStatus";
                break;
            case OperationType.DELETE_REGISTRATIONS:
                suffix = "DeleteRegistrations";
                break;
        }

        assert suffix.length() > 0;

        RecordHistogram.recordEnumeratedHistogram(
                "Conversions.AndroidOperationResult2." + suffix, result, OperationResult.COUNT);
    }

    @CalledByNative
    private AttributionOsLevelManager(long nativePtr) {
        mNativePtr = nativePtr;
    }

    private MeasurementManagerFutures getManager() {
        if (!supportsAttribution()) {
            return null;
        }
        if (sManagerForTesting != null) {
            return sManagerForTesting;
        }
        if (mManager != null) {
            return mManager;
        }
        try {
            mManager = MeasurementManagerFutures.from(ContextUtils.getApplicationContext());
        } catch (Throwable t) {
            // An error may be thrown if android.ext.adservices is not loaded.
            Log.i(TAG, "Failed to get measurement manager", t);
        }
        return mManager;
    }

    private void onRegistrationCompleted(
            int requestId, @OperationType int type, @OperationResult int result) {
        recordOperationResult(type, result);

        if (mNativePtr != 0) {
            AttributionOsLevelManagerJni.get()
                    .onRegistrationCompleted(
                            mNativePtr, requestId, result == OperationResult.SUCCESS);
        }
    }

    private void addRegistrationFutureCallback(
            int requestId, @OperationType int type, ListenableFuture<?> future) {
        if (!supportsAttribution()) {
            return;
        }
        Futures.addCallback(
                future,
                new FutureCallback<Object>() {
                    @Override
                    public void onSuccess(Object result) {
                        onRegistrationCompleted(requestId, type, OperationResult.SUCCESS);
                    }

                    @Override
                    public void onFailure(Throwable thrown) {
                        Log.w(TAG, "Failed to register", thrown);
                        onRegistrationCompleted(requestId, type, convertToOperationResult(thrown));
                    }
                },
                ContextUtils.getApplicationContext().getMainExecutor());
    }

    @CalledByNative
    private static List<WebSourceParams> createWebSourceParamsList(int size) {
        if (!supportsAttribution()) {
            return null;
        }
        return new ArrayList<WebSourceParams>(size);
    }

    @CalledByNative
    private static void addWebSourceParams(
            List<WebSourceParams> list, GURL registrationUrl, boolean isDebugKeyAllowed) {
        if (!supportsAttribution()) {
            return;
        }
        list.add(new WebSourceParams(Uri.parse(registrationUrl.getSpec()), isDebugKeyAllowed));
    }

    /**
     * Registers a web attribution source with native, see `registerWebSourceAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @CalledByNative
    private void registerWebAttributionSource(
            int requestId, List<WebSourceParams> sources, GURL topLevelOrigin, MotionEvent event) {
        if (!supportsAttribution()) {
            onRegistrationCompleted(
                    requestId,
                    OperationType.REGISTER_WEB_SOURCE,
                    OperationResult.ERROR_VERSION_UNSUPPORTED);
            return;
        }
        MeasurementManagerFutures mm = getManager();
        if (mm == null) {
            onRegistrationCompleted(
                    requestId, OperationType.REGISTER_WEB_SOURCE, OperationResult.ERROR_INTERNAL);
            return;
        }
        ListenableFuture<?> future =
                mm.registerWebSourceAsync(
                        new WebSourceRegistrationRequest(
                                sources,
                                Uri.parse(topLevelOrigin.getSpec()),
                                /* inputEvent= */ event,
                                /* appDestination= */ null,
                                /* webDestination= */ null,
                                /* verifiedDestination= */ null));
        addRegistrationFutureCallback(requestId, OperationType.REGISTER_WEB_SOURCE, future);
    }

    /**
     * Registers an attribution source with native, see `registerSourceAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @OptIn(
            markerClass =
                    androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
                            .RegisterSourceOptIn.class)
    @CalledByNative
    private void registerAttributionSource(
            int requestId, @JniType("std::vector") GURL[] registrationUrls, MotionEvent event) {
        if (!supportsAttribution()) {
            onRegistrationCompleted(
                    requestId,
                    OperationType.REGISTER_SOURCE,
                    OperationResult.ERROR_VERSION_UNSUPPORTED);
            return;
        }
        MeasurementManagerFutures mm = getManager();
        if (mm == null) {
            onRegistrationCompleted(
                    requestId, OperationType.REGISTER_SOURCE, OperationResult.ERROR_INTERNAL);
            return;
        }

        ArrayList<Uri> registrationUris = new ArrayList<Uri>(registrationUrls.length);
        for (GURL registrationUrl : registrationUrls) {
            registrationUris.add(Uri.parse(registrationUrl.getSpec()));
        }
        ListenableFuture<?> future =
                mm.registerSourceAsync(new SourceRegistrationRequest(registrationUris, event));
        addRegistrationFutureCallback(requestId, OperationType.REGISTER_SOURCE, future);
    }

    @CalledByNative
    private static List<WebTriggerParams> createWebTriggerParamsList(int size) {
        if (!supportsAttribution()) {
            return null;
        }
        return new ArrayList<WebTriggerParams>(size);
    }

    @CalledByNative
    private static void addWebTriggerParams(
            List<WebTriggerParams> list, GURL registrationUrl, boolean isDebugKeyAllowed) {
        if (!supportsAttribution()) {
            return;
        }
        list.add(new WebTriggerParams(Uri.parse(registrationUrl.getSpec()), isDebugKeyAllowed));
    }

    /**
     * Registers a web attribution trigger with native, see `registerWebTriggerAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @CalledByNative
    private void registerWebAttributionTrigger(
            int requestId, List<WebTriggerParams> triggers, GURL topLevelOrigin) {
        if (!supportsAttribution()) {
            onRegistrationCompleted(
                    requestId,
                    OperationType.REGISTER_WEB_TRIGGER,
                    OperationResult.ERROR_VERSION_UNSUPPORTED);
            return;
        }

        MeasurementManagerFutures mm = getManager();
        if (mm == null) {
            onRegistrationCompleted(
                    requestId, OperationType.REGISTER_WEB_TRIGGER, OperationResult.ERROR_INTERNAL);
            return;
        }
        ListenableFuture<?> future =
                mm.registerWebTriggerAsync(
                        new WebTriggerRegistrationRequest(
                                triggers, Uri.parse(topLevelOrigin.getSpec())));
        addRegistrationFutureCallback(requestId, OperationType.REGISTER_WEB_TRIGGER, future);
    }

    /**
     * Registers an attribution trigger with native, see `registerTriggerAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @CalledByNative
    private void registerAttributionTrigger(int requestId, GURL registrationUrl) {
        if (!supportsAttribution()) {
            onRegistrationCompleted(
                    requestId,
                    OperationType.REGISTER_TRIGGER,
                    OperationResult.ERROR_VERSION_UNSUPPORTED);
            return;
        }

        MeasurementManagerFutures mm = getManager();
        if (mm == null) {
            onRegistrationCompleted(
                    requestId, OperationType.REGISTER_TRIGGER, OperationResult.ERROR_INTERNAL);
            return;
        }
        ListenableFuture<?> future = mm.registerTriggerAsync(Uri.parse(registrationUrl.getSpec()));
        addRegistrationFutureCallback(requestId, OperationType.REGISTER_TRIGGER, future);
    }

    private void onDataDeletionCompleted(int requestId) {
        if (mNativePtr != 0) {
            AttributionOsLevelManagerJni.get().onDataDeletionCompleted(mNativePtr, requestId);
        }
    }

    /**
     * Deletes attribution data with native, see `deleteRegistrationsAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @CalledByNative
    private void deleteRegistrations(
            int requestId,
            long startMs,
            long endMs,
            @JniType("std::vector") GURL[] origins,
            @JniType("std::vector<std::string>") String[] domains,
            int deletionMode,
            int matchBehavior) {
        if (!supportsAttribution()) {
            recordOperationResult(
                    OperationType.DELETE_REGISTRATIONS, OperationResult.ERROR_VERSION_UNSUPPORTED);
            onDataDeletionCompleted(requestId);
            return;
        }
        MeasurementManagerFutures mm = getManager();
        if (mm == null) {
            recordOperationResult(
                    OperationType.DELETE_REGISTRATIONS, OperationResult.ERROR_INTERNAL);
            onDataDeletionCompleted(requestId);
            return;
        }

        // Currently Android and Chromium have different matching behaviors when both
        // `origins` and `domains` are empty.
        // Chromium: Delete -> Delete nothing; Preserve -> Delete all.
        // Android: Delete -> Delete all; Preserve -> Delete nothing.
        // Android may fix the behavior in the future. As a workaround, Chromium will
        // not call Android if it's to delete nothing (no-op), and call Android with
        // both Delete and Preserve modes if it's to delete all. These two modes will
        // be one no-op and one delete all in Android releases with and without the
        // fix. See crbug.com/1442967.

        ImmutableList<Integer> matchBehaviors = null;

        if (origins.length == 0 && domains.length == 0) {
            switch (matchBehavior) {
                case DeletionRequest.MATCH_BEHAVIOR_DELETE:
                    recordOperationResult(
                            OperationType.DELETE_REGISTRATIONS, OperationResult.SUCCESS);
                    onDataDeletionCompleted(requestId);
                    return;
                case DeletionRequest.MATCH_BEHAVIOR_PRESERVE:
                    matchBehaviors =
                            ImmutableList.of(
                                    DeletionRequest.MATCH_BEHAVIOR_DELETE,
                                    DeletionRequest.MATCH_BEHAVIOR_PRESERVE);
                    break;
                default:
                    Log.e(TAG, "Received invalid match behavior: ", matchBehavior);
                    recordOperationResult(
                            OperationType.DELETE_REGISTRATIONS, OperationResult.ERROR_UNKNOWN);
                    onDataDeletionCompleted(requestId);
                    return;
            }
        } else {
            matchBehaviors = ImmutableList.of(matchBehavior);
        }

        ArrayList<Uri> originUris = new ArrayList<Uri>(origins.length);
        for (GURL origin : origins) {
            originUris.add(Uri.parse(origin.getSpec()));
        }

        ArrayList<Uri> domainUris = new ArrayList<Uri>(domains.length);
        for (String domain : domains) {
            domainUris.add(Uri.parse(domain));
        }

        int numCalls = matchBehaviors.size();

        FutureCallback<Object> callback =
                new FutureCallback<Object>() {
                    private int mNumPendingCalls = numCalls;

                    private void onCall() {
                        if (--mNumPendingCalls == 0) {
                            onDataDeletionCompleted(requestId);
                        }
                    }

                    @Override
                    public void onSuccess(Object result) {
                        recordOperationResult(
                                OperationType.DELETE_REGISTRATIONS, OperationResult.SUCCESS);
                        onCall();
                    }

                    @Override
                    public void onFailure(Throwable thrown) {
                        Log.w(TAG, "Failed to delete measurement API data", thrown);
                        recordOperationResult(
                                OperationType.DELETE_REGISTRATIONS,
                                convertToOperationResult(thrown));
                        onCall();
                    }
                };

        for (int currMatchBehavior : matchBehaviors) {
            ListenableFuture<?> future =
                    mm.deleteRegistrationsAsync(
                            new DeletionRequest(
                                    deletionMode,
                                    currMatchBehavior,
                                    Instant.ofEpochMilli(startMs),
                                    Instant.ofEpochMilli(endMs),
                                    originUris,
                                    domainUris));

            Futures.addCallback(
                    future, callback, ContextUtils.getApplicationContext().getMainExecutor());
        }
    }

    private static void onMeasurementStateReturned(int status, @OperationResult int result) {
        recordOperationResult(OperationType.GET_MEASUREMENT_API_STATUS, result);
        AttributionOsLevelManagerJni.get().onMeasurementStateReturned(status);
    }

    /**
     * Gets Measurement API status with native, see `getMeasurementApiStatusAsync()`:
     * https://developer.android.com/reference/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.
     */
    @CalledByNative
    private static void getMeasurementApiStatus() {
        ThreadUtils.assertOnBackgroundThread();

        if (sManagerForTesting != null) {
            AttributionOsLevelManagerJni.get().onMeasurementStateReturned(1);
            return;
        }

        if (!supportsAttribution()) {
            onMeasurementStateReturned(/* status= */ 0, OperationResult.ERROR_VERSION_UNSUPPORTED);
            return;
        }
        if (ContextUtils.getApplicationContext()
                        .checkPermission(
                                PERMISSION_ACCESS_ADSERVICES_ATTRIBUTION,
                                Process.myPid(),
                                Process.myUid())
                != PackageManager.PERMISSION_GRANTED) {
            // Permission may not be granted when embedded as WebView.
            onMeasurementStateReturned(/* status= */ 0, OperationResult.ERROR_PERMISSION_UNGRANTED);
            return;
        }
        MeasurementManagerFutures mm = null;
        try {
            mm = MeasurementManagerFutures.from(ContextUtils.getApplicationContext());
        } catch (Throwable t) {
            // An error may be thrown if android.ext.adservices is not loaded.
            Log.i(TAG, "Failed to get measurement manager", t);
        }

        if (mm == null) {
            onMeasurementStateReturned(/* status= */ 0, OperationResult.ERROR_INTERNAL);
            return;
        }

        ListenableFuture<Integer> future = null;
        try {
            future = mm.getMeasurementApiStatusAsync();
        } catch (IllegalStateException ex) {
            // An illegal state exception may be thrown for some versions of the underlying
            // Privacy Sandbox SDK.
            Log.i(TAG, "Failed to get measurement API status", ex);
        }

        if (future == null) {
            onMeasurementStateReturned(/* status= */ 0, OperationResult.ERROR_INTERNAL);
            return;
        }

        Futures.addCallback(
                future,
                new FutureCallback<Integer>() {
                    @Override
                    public void onSuccess(Integer status) {
                        onMeasurementStateReturned(status, OperationResult.SUCCESS);
                    }

                    @Override
                    public void onFailure(Throwable thrown) {
                        Log.w(TAG, "Failed to get measurement API status", thrown);
                        onMeasurementStateReturned(
                                /* status= */ 0, convertToOperationResult(thrown));
                    }
                },
                ContextUtils.getApplicationContext().getMainExecutor());
    }

    @CalledByNative
    private void nativeDestroyed() {
        mNativePtr = 0;
    }

    public static void setManagerForTesting(MeasurementManagerFutures manager) {
        sManagerForTesting = manager;
        PostTask.postTask(
                TaskTraits.BEST_EFFORT, () -> AttributionOsLevelManager.getMeasurementApiStatus());
        ResettersForTesting.register(
                () -> {
                    sManagerForTesting = null;
                    PostTask.postTask(
                            TaskTraits.BEST_EFFORT,
                            () -> AttributionOsLevelManager.getMeasurementApiStatus());
                });
    }

    @NativeMethods
    interface Natives {
        void onDataDeletionCompleted(long nativeAttributionOsLevelManagerAndroid, int requestId);

        void onRegistrationCompleted(
                long nativeAttributionOsLevelManagerAndroid, int requestId, boolean success);

        void onMeasurementStateReturned(int state);
    }
}