/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.appsearch;

import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
import static android.app.appsearch.AppSearchResult.RESULT_DENIED;
import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
import static android.app.appsearch.AppSearchResult.RESULT_RATE_LIMITED;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_WRONLY;

import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_DENYLIST;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ISOLATED_STORAGE_DISABLED;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_API_COSTS;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_ENABLED;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.UiAutomation;
import android.app.admin.DevicePolicyManager;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchEnvironmentFactory;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.AppSearchSchema.LongPropertyConfig;
import android.app.appsearch.AppSearchSchema.PropertyConfig;
import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
import android.app.appsearch.FrameworkAppSearchEnvironment;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.GetByDocumentIdRequest;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.InternalSetSchemaResponse;
import android.app.appsearch.RemoveByDocumentIdRequest;
import android.app.appsearch.ReportUsageRequest;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SearchSuggestionSpec;
import android.app.appsearch.aidl.AppSearchAttributionSource;
import android.app.appsearch.aidl.AppSearchBatchResultParcel;
import android.app.appsearch.aidl.AppSearchResultParcel;
import android.app.appsearch.aidl.AppSearchResultParcelV2;
import android.app.appsearch.aidl.DocumentsParcel;
import android.app.appsearch.aidl.GetDocumentsAidlRequest;
import android.app.appsearch.aidl.GetNamespacesAidlRequest;
import android.app.appsearch.aidl.GetNextPageAidlRequest;
import android.app.appsearch.aidl.GetSchemaAidlRequest;
import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
import android.app.appsearch.aidl.GlobalSearchAidlRequest;
import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
import android.app.appsearch.aidl.IAppSearchManager;
import android.app.appsearch.aidl.IAppSearchObserverProxy;
import android.app.appsearch.aidl.IAppSearchResultCallback;
import android.app.appsearch.aidl.InitializeAidlRequest;
import android.app.appsearch.aidl.InvalidateNextPageTokenAidlRequest;
import android.app.appsearch.aidl.PersistToDiskAidlRequest;
import android.app.appsearch.aidl.PutDocumentsAidlRequest;
import android.app.appsearch.aidl.PutDocumentsFromFileAidlRequest;
import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
import android.app.appsearch.aidl.ReportUsageAidlRequest;
import android.app.appsearch.aidl.SearchAidlRequest;
import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
import android.app.appsearch.aidl.SetSchemaAidlRequest;
import android.app.appsearch.aidl.UnregisterObserverCallbackAidlRequest;
import android.app.appsearch.aidl.WriteSearchResultsToFileAidlRequest;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.observer.ObserverSpec;
import android.app.appsearch.safeparcel.GenericDocumentParcel;
import android.app.appsearch.stats.SchemaMigrationStats;
import android.app.appsearch.testutil.AppSearchTestUtils;
import android.app.role.RoleManager;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.provider.DeviceConfig;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.appsearch.flags.Flags;
import com.android.modules.utils.testing.ExtendedMockitoRule;
import com.android.modules.utils.testing.TestableDeviceConfig;
import com.android.server.appsearch.ServiceTestUtil.MockServiceManager;
import com.android.server.appsearch.external.localstorage.stats.CallStats;
import com.android.server.appsearch.external.localstorage.stats.QueryStats;
import com.android.server.appsearch.external.localstorage.stats.SearchIntentStats;
import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
import com.android.server.appsearch.external.localstorage.usagereporting.ClickActionGenericDocument;
import com.android.server.appsearch.external.localstorage.usagereporting.SearchActionGenericDocument;
import com.android.server.appsearch.isolated_storage_service.IsolatedStorageServiceManager;
import com.android.server.appsearch.util.ExecutorManager;

import libcore.io.IoBridge;

import com.google.common.util.concurrent.SettableFuture;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;

import java.io.File;
import java.io.FileDescriptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;

public class AppSearchManagerServiceTest {
    private static final String DATABASE_NAME = "databaseName";
    private static final String NAMESPACE = "namespace";
    private static final String ID = "ID";
    private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build();
    // Mostly guarantees the logged estimated binder latency is positive and doesn't overflow
    private static final long BINDER_CALL_START_TIME = SystemClock.elapsedRealtime() - 1;
    private static final String FOO_PACKAGE_NAME = "foo";

    private final MockServiceManager mMockServiceManager = new MockServiceManager();
    private final RoleManager mRoleManager = mock(RoleManager.class);
    private final DevicePolicyManager mDevicePolicyManager = mock(DevicePolicyManager.class);

    @Rule public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();

    @Rule
    public ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder()
            .addStaticMockFixtures(() -> mMockServiceManager, TestableDeviceConfig::new)
            .build();

    @Rule
    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();

    private TestContext mContext;
    private AppSearchManagerService mAppSearchManagerService;
    private UserHandle mUserHandle;
    private UiAutomation mUiAutomation;
    private IAppSearchManager.Stub mAppSearchManagerServiceStub;
    private AppSearchUserInstance mUserInstance;
    private InternalAppSearchLogger mLogger;
    private ExecutorManager mExecutorManager;

    private int mCallingPid;

    @Before
    public void setUp() throws Exception {
        Context context = ApplicationProvider.getApplicationContext();
        mUserHandle = context.getUser();
        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
        final boolean isIsolatedStorageAvailable = false;
        mContext =
                new TestContext(
                        context, mRoleManager, mDevicePolicyManager, isIsolatedStorageAvailable);

        // Set a test environment that provides a temporary folder for AppSearch
        File mAppSearchDir = mTemporaryFolder.newFolder();
        AppSearchEnvironmentFactory.setEnvironmentInstanceForTest(
                new FrameworkAppSearchEnvironment() {
                    @Override
                    public File getAppSearchDir(@NonNull Context unused,
                            @NonNull UserHandle userHandle) {
                        return mAppSearchDir;
                    }
                });


        // In AppSearchManagerService, ServiceAppSearchConfig is a singleton. During tearDown for
        // TestableDeviceConfig, the propertyChangedListeners are removed. Therefore we have to set
        // a fresh config with listeners in setUp in order to set new properties.
        ServiceAppSearchConfig appSearchConfig =
                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR, context);
        AppSearchComponentFactory.setConfigInstanceForTest(appSearchConfig);

        // Create the user instance and add a spy to its logger to verify logging
        // Note, SimpleTestLogger does not suffice for our tests since CallStats logging in
        // AppSearchManagerService occurs in a separate thread. With a spy, we can verify with a
        // timeout to catch asynchronous calls.
        mExecutorManager =
                new ExecutorManager(appSearchConfig, /* isIsolatedStorageAvailable= */ true);
        mUserInstance =
                AppSearchUserInstanceManager.getInstance()
                        .getOrCreateUserInstance(
                                mContext, mUserHandle, appSearchConfig, mExecutorManager, null);
        mLogger = spy(mUserInstance.getLogger());
        mUserInstance.setLoggerForTest(mLogger);

        // Start the service
        mAppSearchManagerService =
                new AppSearchManagerService(mContext, new AppSearchModule.Lifecycle(mContext));
        mAppSearchManagerService.onStart();
        mAppSearchManagerServiceStub = mMockServiceManager.mStubCaptor.getValue();
        assertThat(mAppSearchManagerServiceStub).isNotNull();
        mCallingPid = android.os.Process.myPid();
    }

    @After
    public void tearDown() {
        // The TemporaryFolder rule's teardown will delete the current test folder; by removing the
        // current user instance, the next test will be able to create a new AppSearchImpl with
        // a new test folder
        AppSearchUserInstanceManager.getInstance()
                .closeAndRemoveUserInstance(mUserHandle, /* removeUserData= */ true);
    }

    @Test
    public void testCallingPackageDoesntExistsInTargetUser() throws Exception {
        UserHandle testTargetUser = new UserHandle(1234);
        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.INTERACT_ACROSS_USERS_FULL);
        try {
            // Try to initial a AppSearchSession for secondary user, but the calling package doesn't
            // exist in there.
            TestResultCallback callback = new TestResultCallback();
            mAppSearchManagerServiceStub.initialize(
                    new InitializeAidlRequest(
                            AppSearchAttributionSource.createAttributionSource(mContext,
                                    mCallingPid),
                            testTargetUser, System.currentTimeMillis())
                    , callback);
            assertThat(callback.get().isSuccess()).isFalse();
            assertThat(callback.get().getErrorMessage()).contains(
                    "SecurityException: Package: " + mContext.getPackageName()
                            + " haven't installed for user " + testTargetUser.getIdentifier());
        } finally {
            mUiAutomation.dropShellPermissionIdentity();
        }
    }

    @Test
    public void testSetSchemaStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.setSchema(
                new SetSchemaAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                        DATABASE_NAME,
                /* schemaBundles= */ Collections.emptyList(),
                /* visibilityBundles= */ Collections.emptyList(), /* forceOverride= */ false,
                /* schemaVersion= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_SET_SCHEMA, callReceivedTimestampMillis);
        // SetSchemaStats is also logged in SetSchema
        ArgumentCaptor<SetSchemaStats> setSchemaStatsCaptor = ArgumentCaptor.forClass(
                SetSchemaStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(setSchemaStatsCaptor.capture());
        SetSchemaStats setSchemaStats = setSchemaStatsCaptor.getValue();
        assertThat(setSchemaStats.getPackageName()).isEqualTo(mContext.getPackageName());
        assertThat(setSchemaStats.getDatabase()).isEqualTo(DATABASE_NAME);
        assertThat(setSchemaStats.getStatusCode()).isEqualTo(callback.get().getResultCode());
        assertThat(setSchemaStats.getSchemaMigrationCallType()).isEqualTo(
                SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE);
    }

    @Test
    public void testLocalGetSchemaStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                        BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GET_SCHEMA, callReceivedTimestampMillis);
    }

    @Test
    public void testGlobalGetSchemaStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        String otherPackageName = mContext.getPackageName() + "foo";
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        otherPackageName, DATABASE_NAME, mUserHandle, BINDER_CALL_START_TIME,
                        /* isForEnterprise= */ false),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GLOBAL_GET_SCHEMA, callReceivedTimestampMillis);
    }

    @Test
    public void testGetNamespacesStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNamespaces(
                new GetNamespacesAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GET_NAMESPACES, callReceivedTimestampMillis);
    }

    @Test
    public void testPutDocumentsStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.putDocuments(
                new PutDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        new DocumentsParcel(Collections.emptyList(), Collections.emptyList()),
                        mUserHandle, BINDER_CALL_START_TIME), callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_PUT_DOCUMENTS, callReceivedTimestampMillis);
        // putDocuments only logs PutDocumentStats indirectly so we don't verify it
    }

    @Test
    public void testPutDocumentsStatsLogging_takenActions() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        // Set SearchAction and ClickAction schemas.
        List<AppSearchSchema> schemas =
                Arrays.asList(
                        new AppSearchSchema.Builder("builtin:SearchAction")
                                .addProperty(
                                        new LongPropertyConfig.Builder("actionType")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new StringPropertyConfig.Builder("query")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new LongPropertyConfig.Builder("fetchedResultCount")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .build(),
                        new AppSearchSchema.Builder("builtin:ClickAction")
                                .addProperty(
                                        new LongPropertyConfig.Builder("actionType")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new StringPropertyConfig.Builder("query")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new LongPropertyConfig.Builder("resultRankInBlock")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new LongPropertyConfig.Builder("resultRankGlobal")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .addProperty(
                                        new LongPropertyConfig.Builder("timeStayOnResultMillis")
                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                                                .build())
                                .build());
        InternalSetSchemaResponse internalSetSchemaResponse =
                mUserInstance
                        .getAppSearchImpl()
                        .setSchema(
                                mContext.getPackageName(),
                                DATABASE_NAME,
                                schemas,
                                /* visibilityDocuments= */ Collections.emptyList(),
                                /* forceOverride= */ false,
                                /* version= */ 0,
                                /* setSchemaStatsBuilder= */ null,
                                /* callStatsBuilder= */ null);
        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();

        // Prepare search action and click action generic documents.
        SearchActionGenericDocument searchAction1 =
                new SearchActionGenericDocument.Builder(
                                "namespace", "search1", "builtin:SearchAction")
                        .setCreationTimestampMillis(1000)
                        .setQuery("tes")
                        .setFetchedResultCount(20)
                        .build();
        ClickActionGenericDocument clickAction1 =
                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
                        .setCreationTimestampMillis(2000)
                        .setQuery("tes")
                        .setResultRankInBlock(1)
                        .setResultRankGlobal(2)
                        .setTimeStayOnResultMillis(512)
                        .build();
        ClickActionGenericDocument clickAction2 =
                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
                        .setCreationTimestampMillis(3000)
                        .setQuery("tes")
                        .setResultRankInBlock(3)
                        .setResultRankGlobal(6)
                        .setTimeStayOnResultMillis(1024)
                        .build();
        SearchActionGenericDocument searchAction2 =
                new SearchActionGenericDocument.Builder(
                                "namespace", "search2", "builtin:SearchAction")
                        .setCreationTimestampMillis(5000)
                        .setQuery("test")
                        .setFetchedResultCount(10)
                        .build();
        ClickActionGenericDocument clickAction3 =
                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
                        .setCreationTimestampMillis(6000)
                        .setQuery("test")
                        .setResultRankInBlock(2)
                        .setResultRankGlobal(4)
                        .setTimeStayOnResultMillis(512)
                        .build();
        List<GenericDocumentParcel> takenActionGenericDocumentParcels =
                Arrays.asList(
                        GenericDocumentParcel.fromGenericDocument(searchAction1),
                        GenericDocumentParcel.fromGenericDocument(clickAction1),
                        GenericDocumentParcel.fromGenericDocument(clickAction2),
                        GenericDocumentParcel.fromGenericDocument(searchAction2),
                        GenericDocumentParcel.fromGenericDocument(clickAction3));

        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.putDocuments(
                new PutDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                        DATABASE_NAME,
                        new DocumentsParcel(
                                Collections.emptyList(), takenActionGenericDocumentParcels),
                        mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_PUT_DOCUMENTS, callReceivedTimestampMillis);

        // Verify search sessions.
        ArgumentCaptor<List<SearchSessionStats>> searchSessionsStatsCaptor =
                ArgumentCaptor.forClass(List.class);
        verify(mLogger, timeout(1000).times(1)).logStats(searchSessionsStatsCaptor.capture());
        List<SearchSessionStats> searchSessionsStats = searchSessionsStatsCaptor.getValue();

        assertThat(searchSessionsStats).hasSize(1);
        assertThat(searchSessionsStats.get(0).getPackageName())
                .isEqualTo(mContext.getPackageName());
        assertThat(searchSessionsStats.get(0).getDatabase()).isEqualTo(DATABASE_NAME);

        // Verify search intents.
        List<SearchIntentStats> searchIntentsStats =
                searchSessionsStats.get(0).getSearchIntentsStats();
        assertThat(searchIntentsStats).hasSize(2);

        assertThat(searchIntentsStats.get(0).getPackageName()).isEqualTo(mContext.getPackageName());
        assertThat(searchIntentsStats.get(0).getDatabase()).isEqualTo(DATABASE_NAME);
        assertThat(searchIntentsStats.get(0).getPrevQuery()).isNull();
        assertThat(searchIntentsStats.get(0).getCurrQuery()).isEqualTo("tes");
        assertThat(searchIntentsStats.get(0).getTimestampMillis()).isEqualTo(1000);
        assertThat(searchIntentsStats.get(0).getNumResultsFetched()).isEqualTo(20);
        assertThat(searchIntentsStats.get(0).getQueryCorrectionType())
                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
        assertThat(searchIntentsStats.get(0).getClicksStats()).hasSize(2);

        assertThat(searchIntentsStats.get(1).getPackageName()).isEqualTo(mContext.getPackageName());
        assertThat(searchIntentsStats.get(1).getDatabase()).isEqualTo(DATABASE_NAME);
        assertThat(searchIntentsStats.get(1).getPrevQuery()).isEqualTo("tes");
        assertThat(searchIntentsStats.get(1).getCurrQuery()).isEqualTo("test");
        assertThat(searchIntentsStats.get(1).getTimestampMillis()).isEqualTo(5000);
        assertThat(searchIntentsStats.get(1).getNumResultsFetched()).isEqualTo(10);
        assertThat(searchIntentsStats.get(1).getQueryCorrectionType())
                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
        assertThat(searchIntentsStats.get(1).getClicksStats()).hasSize(1);
    }

    @Test
    public void testLocalGetDocumentsStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GET_DOCUMENTS, callReceivedTimestampMillis);
    }

    @Test
    public void testGlobalGetDocumentsStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        String otherPackageName = mContext.getPackageName() + "foo";
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        otherPackageName, DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                       mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        verifyCallStats(
                mContext.getPackageName(),
                DATABASE_NAME,
                CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID,
                callReceivedTimestampMillis);
    }

    @Test
    public void testSearchStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.search(
                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext,
                        mCallingPid),
                        DATABASE_NAME, /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                        BINDER_CALL_START_TIME), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_SEARCH, callReceivedTimestampMillis);
        // search only logs SearchStats indirectly so we don't verify it
    }

    @Test
    public void testGlobalSearchStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ false), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_GLOBAL_SEARCH, callReceivedTimestampMillis);
        // globalSearch only logs SearchStats indirectly so we don't verify it
    }

    @Test
    public void testLocalGetNextPageStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                DATABASE_NAME, /* nextPageToken= */ 0,
                AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GET_NEXT_PAGE, callReceivedTimestampMillis);
        // getNextPage also logs QueryStats
        ArgumentCaptor<QueryStats> queryStatsCaptor = ArgumentCaptor.forClass(QueryStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(queryStatsCaptor.capture());
        QueryStats queryStats = queryStatsCaptor.getValue();
        assertThat(queryStats.getVisibilityScope()).isEqualTo(QueryStats.VISIBILITY_SCOPE_LOCAL);
        assertThat(queryStats.getPackageName()).isEqualTo(mContext.getPackageName());
        assertThat(queryStats.getDatabase()).isEqualTo(DATABASE_NAME);
        assertThat(queryStats.getJoinType())
                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
    }

    @Test
    public void testGlobalGetNextPageStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* databaseName= */ null, /* nextPageToken= */ 0,
                AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_GLOBAL_GET_NEXT_PAGE, callReceivedTimestampMillis);
        // getNextPage also logs QueryStats
        ArgumentCaptor<QueryStats> queryStatsCaptor = ArgumentCaptor.forClass(QueryStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(queryStatsCaptor.capture());
        QueryStats queryStats = queryStatsCaptor.getValue();
        assertThat(queryStats.getVisibilityScope()).isEqualTo(QueryStats.VISIBILITY_SCOPE_GLOBAL);
        assertThat(queryStats.getPackageName()).isEqualTo(mContext.getPackageName());
        assertThat(queryStats.getDatabase()).isNull();
        assertThat(queryStats.getJoinType())
                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
    }

    @Test
    public void testInvalidateNextPageTokenStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ false));
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, callReceivedTimestampMillis);
    }

    @Test
    public void testWriteSearchResultsToFileStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        File tempFile = mTemporaryFolder.newFile();
        FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_WRONLY);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.writeSearchResultsToFile(
                new WriteSearchResultsToFileAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        new ParcelFileDescriptor(fd), /* searchExpression= */ "", EMPTY_SEARCH_SPEC,
                        mUserHandle, BINDER_CALL_START_TIME), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(),
                DATABASE_NAME,
                CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
                callReceivedTimestampMillis);
    }

    @Test
    public void testPutDocumentsFromFileStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        File tempFile = mTemporaryFolder.newFile();
        FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_RDONLY);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.putDocumentsFromFile(new PutDocumentsFromFileAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                DATABASE_NAME,
                new ParcelFileDescriptor(fd), mUserHandle,
                new SchemaMigrationStats.Builder(mContext.getPackageName(), DATABASE_NAME).build(),
                /* totalLatencyStartTimeMillis= */ 0, BINDER_CALL_START_TIME), callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(),
                DATABASE_NAME,
                CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE, callReceivedTimestampMillis);
        // putDocumentsFromFile also logs SchemaMigrationStats
        ArgumentCaptor<SchemaMigrationStats> migrationStatsCaptor = ArgumentCaptor.forClass(
                SchemaMigrationStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(migrationStatsCaptor.capture());
        SchemaMigrationStats migrationStats = migrationStatsCaptor.getValue();
        assertThat(migrationStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
        assertThat(migrationStats.getSaveDocumentLatencyMillis()).isGreaterThan(0);
        // putDocumentsFromFile only logs PutDocumentStats indirectly so we don't verify it
    }

    @Test
    public void testSearchSuggestionStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        SearchSuggestionSpec searchSuggestionSpec =
            new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1).build();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.searchSuggestion(
                new SearchSuggestionAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        DATABASE_NAME, /* suggestionQueryExpression= */ "foo", searchSuggestionSpec,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_SEARCH_SUGGESTION, callReceivedTimestampMillis);
    }

    @Test
    public void testLocalReportUsageStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        setUpTestSchema(mContext.getPackageName(), DATABASE_NAME);
        setUpTestDocument(mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.reportUsage(
                new ReportUsageAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME,
                        new ReportUsageRequest.Builder(NAMESPACE, ID)
                                .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
                                .build(),
                        /* systemUsage= */ false, mUserHandle, BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_REPORT_USAGE, callReceivedTimestampMillis);
        removeTestSchema(mContext.getPackageName(), DATABASE_NAME);
    }

    @Test
    public void testGlobalReportUsageStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        // Grant system access for global report usage
        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA);
        try {
            String otherPackageName = mContext.getPackageName() + "foo";
            setUpTestSchema(otherPackageName, DATABASE_NAME);
            setUpTestDocument(otherPackageName, DATABASE_NAME, NAMESPACE, ID);
            TestResultCallback callback = new TestResultCallback();
            mAppSearchManagerServiceStub.reportUsage(
                    new ReportUsageAidlRequest(
                            AppSearchAttributionSource.createAttributionSource(mContext,
                                    mCallingPid),
                            otherPackageName, DATABASE_NAME,
                            new ReportUsageRequest.Builder(NAMESPACE, ID)
                                    .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
                                    .build(),
                            /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME),
                    callback);
            assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
            verifyCallStats(
                    mContext.getPackageName(),
                    DATABASE_NAME,
                    CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE, callReceivedTimestampMillis);
            removeTestSchema(otherPackageName, DATABASE_NAME);
        } finally {
            mUiAutomation.dropShellPermissionIdentity();
        }
    }

    @Test
    public void testRemoveByDocumentIdStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.removeByDocumentId(
                new RemoveByDocumentIdAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        DATABASE_NAME,
                        new RemoveByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                        mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        verifyCallStats(
                mContext.getPackageName(),
                DATABASE_NAME,
                CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, callReceivedTimestampMillis);
    }

    @Test
    public void testRemoveByQueryStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.removeByQuery(
                new RemoveByQueryAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        /* queryExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(),
                DATABASE_NAME,
                CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH, callReceivedTimestampMillis);
    }

    @Test
    public void testGetStorageInfoStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getStorageInfo(
                new GetStorageInfoAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_GET_STORAGE_INFO, callReceivedTimestampMillis);
    }

    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_NO_OP_MANUAL_PERSIST)
    @Test
    public void testPersistToDiskStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        mAppSearchManagerServiceStub.persistToDisk(
                new PersistToDiskAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), mUserHandle,
                        BINDER_CALL_START_TIME));
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_FLUSH, callReceivedTimestampMillis);
    }

    @Test
    public void testRegisterObserverCallbackStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        AppSearchResultParcel<Void> resultParcel =
                mAppSearchManagerServiceStub.registerObserverCallback(
                        new RegisterObserverCallbackAidlRequest(
                                AppSearchAttributionSource.createAttributionSource(mContext,
                                        mCallingPid),
                                mContext.getPackageName(),
                                new ObserverSpec.Builder().build(),
                                mUserHandle, BINDER_CALL_START_TIME),
                        new IAppSearchObserverProxy.Stub() {
                            @Override
                            public void onSchemaChanged(String packageName, String databaseName,
                                    List<String> changedSchemaNames) throws RemoteException {
                            }

                            @Override
                            public void onDocumentChanged(String packageName, String databaseName,
                                    String namespace, String schemaName,
                                    List<String> changedDocumentIds) throws RemoteException {
                            }
                        });
        assertThat(resultParcel.getResult().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_REGISTER_OBSERVER_CALLBACK, callReceivedTimestampMillis);
    }

    @Test
    public void testUnregisterObserverCallbackStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        AppSearchResultParcel<Void> resultParcel =
                mAppSearchManagerServiceStub.unregisterObserverCallback(
                        new UnregisterObserverCallbackAidlRequest(
                                AppSearchAttributionSource.createAttributionSource(mContext,
                                        mCallingPid),
                                mContext.getPackageName(), mUserHandle,
                                BINDER_CALL_START_TIME),
                        new IAppSearchObserverProxy.Stub() {
                            @Override
                            public void onSchemaChanged(String packageName, String databaseName,
                                    List<String> changedSchemaNames) throws RemoteException {
                            }

                            @Override
                            public void onDocumentChanged(String packageName, String databaseName,
                                    String namespace, String schemaName,
                                    List<String> changedDocumentIds) throws RemoteException {
                            }
                        });
        assertThat(resultParcel.getResult().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(
                mContext.getPackageName(), CallStats.CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK, callReceivedTimestampMillis);
    }

    @Test
    public void testInitializeStatsLogging() throws Exception {
        final long callReceivedTimestampMillis = System.currentTimeMillis();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.initialize(
                new InitializeAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_INITIALIZE, callReceivedTimestampMillis);
        // initialize only logs InitializeStats indirectly so we don't verify it
    }

    private void verifyCallStats(String packageName, String databaseName, int callType, long expectedCallReceivedTimestampMillis) {
        ArgumentCaptor<CallStats> captor = ArgumentCaptor.forClass(CallStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(captor.capture());
        CallStats callStats = captor.getValue();
        assertThat(callStats.getPackageName()).isEqualTo(packageName);
        assertThat(callStats.getDatabase()).isEqualTo(databaseName);
        assertThat(callStats.getCallType()).isEqualTo(callType);
        assertThat(callStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
        assertThat(callStats.getEstimatedBinderLatencyMillis()).isGreaterThan(0);
        assertThat(callStats.getCallReceivedTimestampMillis()).isAtLeast(expectedCallReceivedTimestampMillis);
        assertThat(callStats.getCallReceivedTimestampMillis()).isAtMost(expectedCallReceivedTimestampMillis + 1000);
    }

    private void verifyCallStats(String packageName, int callType, long expectedCallReceivedTimestampMillis) {
        verifyCallStats(packageName, /* databaseName= */ null, callType, expectedCallReceivedTimestampMillis);
    }

    @Test
    public void testDenylistMatchingCallingPackage() throws Exception {
        String denylistString =
                "pkg=com.android.appsearch.mockingservicestests&apis=localSetSchema,"
                        + "globalGetSchema,localGetSchema,localGetNamespaces,localPutDocuments,"
                        + "globalGetDocuments,localGetDocuments,localSearch,globalSearch,"
                        + "globalGetNextPage,localGetNextPage,invalidateNextPageToken,"
                        + "localWriteSearchResultsToFile,localPutDocumentsFromFile,"
                        + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                        + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                        + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
                        + "initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        // We expect all local calls (pkg+db) and global calls (pkg only) to be denied since the
        // denylist denies all api's for our calling package
        verifyLocalCallsResults(RESULT_DENIED);
        verifyGlobalCallsResults(RESULT_DENIED);
    }

    @Test
    public void testDenylistNonMatchingCallingPackage() throws Exception {
        String denylistString =
                "pkg=foo&apis=localSetSchema,globalGetSchema,localGetSchema,localGetNamespaces,"
                        + "localPutDocuments,globalGetDocuments,localGetDocuments,localSearch,"
                        + "globalSearch,globalGetNextPage,localGetNextPage,invalidateNextPageToken,"
                        + "localWriteSearchResultsToFile,localPutDocumentsFromFile,"
                        + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                        + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                        + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
                        + "initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        // We expect none of the local calls (pkg+db) and global calls (pkg only) to be denied since
        // the denylist denies all api's for a different calling package
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testDenylistMatchingCallingPackageAndDatabase() throws Exception {
        String denylistString =
                "pkg=com.android.appsearch.mockingservicestests&db=databaseName&apis="
                        + "localSetSchema,globalGetSchema,localGetSchema,localGetNamespaces,"
                        + "localPutDocuments,globalGetDocuments,localGetDocuments,localSearch,"
                        + "globalSearch,globalGetNextPage,localGetNextPage,invalidateNextPageToken,"
                        + "localWriteSearchResultsToFile,localPutDocumentsFromFile,"
                        + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                        + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                        + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
                        + "initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        // We expect only the local calls (pkg+db) to be denied since the denylist specifies a
        // package-database name pair
        verifyLocalCallsResults(RESULT_DENIED);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testDenylistNonMatchingCallingPackageAndDatabase() throws Exception {
        // This denylist has two entries both of which should not match any of the api calls below
        // since either the package name or the database name will be different
        String denylistString =
                "pkg=foo&db=databaseName&apis=localSetSchema,globalGetSchema,localGetSchema,"
                        + "localGetNamespaces,localPutDocuments,globalGetDocuments,"
                        + "localGetDocuments,localSearch,globalSearch,globalGetNextPage,"
                        + "localGetNextPage,invalidateNextPageToken,localWriteSearchResultsToFile,"
                        + "localPutDocumentsFromFile,localSearchSuggestion,globalReportUsage,"
                        + "localReportUsage,localRemoveByDocumentId,localRemoveBySearch,"
                        + "localGetStorageInfo,flush,globalRegisterObserverCallback,"
                        + "globalUnregisterObserverCallback,initialize;"
                        + "pkg=com.android.appsearch.mockingservicestests&db=foo&apis="
                        + "localSetSchema,globalGetSchema,localGetSchema,localGetNamespaces,"
                        + "localPutDocuments,globalGetDocuments,localGetDocuments,localSearch,"
                        + "globalSearch,globalGetNextPage,localGetNextPage,invalidateNextPageToken,"
                        + "localWriteSearchResultsToFile,localPutDocumentsFromFile,"
                        + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                        + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                        + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
                        + "initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testDenylistMatchingCallingDatabase() throws Exception {
        String denylistString =
                "db=databaseName&apis=localSetSchema,globalGetSchema,localGetSchema,"
                        + "localGetNamespaces,localPutDocuments,globalGetDocuments,"
                        + "localGetDocuments,localSearch,globalSearch,globalGetNextPage,"
                        + "localGetNextPage,invalidateNextPageToken,localWriteSearchResultsToFile,"
                        + "localPutDocumentsFromFile,localSearchSuggestion,globalReportUsage,"
                        + "localReportUsage,localRemoveByDocumentId,localRemoveBySearch,"
                        + "localGetStorageInfo,flush,globalRegisterObserverCallback,"
                        + "globalUnregisterObserverCallback,initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);

        verifyLocalCallsResults(RESULT_DENIED);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);

        // Mock the package manager to return current uid for package foo; this is necessary to pass
        // call verification using a different package name
        PackageManager spyPackageManager = spy(mContext.getPackageManager());
        int uid = AppSearchAttributionSource.createAttributionSource(mContext,
                mCallingPid).getUid();
        doReturn(uid).when(spyPackageManager).getPackageUid(FOO_PACKAGE_NAME, /* flags= */ 0);
        // Specifically grant permission for report system usage to package foo
        doReturn(PackageManager.PERMISSION_GRANTED).when(spyPackageManager).checkPermission(
                READ_GLOBAL_APP_SEARCH_DATA, FOO_PACKAGE_NAME);
        mContext.mPackageManager = spyPackageManager;
        mContext.mPackageName = FOO_PACKAGE_NAME;

        // Confirm that we're using a different package name
        assertThat(mContext.getPackageName()).isEqualTo(FOO_PACKAGE_NAME);
        assertThat(AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid)
                .getPackageName()).isEqualTo(FOO_PACKAGE_NAME);

        verifyLocalCallsResults(RESULT_DENIED);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testDenylistNonMatchingCallingDatabase() throws Exception {
        String denylistString =
                "db=foo&apis=localSetSchema,globalGetSchema,localGetSchema,localGetNamespaces,"
                        + "localPutDocuments,globalGetDocuments,localGetDocuments,localSearch,"
                        + "globalSearch,globalGetNextPage,localGetNextPage,invalidateNextPageToken,"
                        + "localWriteSearchResultsToFile,localPutDocumentsFromFile,"
                        + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                        + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                        + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
                        + "initialize";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testDenylistSomeApis() throws Exception {
        String denylistString =
                "pkg=com.android.appsearch.mockingservicestests&apis=localSetSchema,localGetSchema,"
                        + "localGetNamespaces";
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_DENYLIST, denylistString, false);
        // Specified APIs
        verifySetSchemaResult(RESULT_DENIED);
        verifyLocalGetSchemaResult(RESULT_DENIED);
        verifyGetNamespacesResult(RESULT_DENIED);
        // Everything else
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetSchemaResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyWriteSearchResultsToFileResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsFromFileResult(AppSearchResult.RESULT_OK);
        verifySearchSuggestionResult(AppSearchResult.RESULT_OK);
        verifyLocalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyRemoveByDocumentIdResult(AppSearchResult.RESULT_OK);
        verifyRemoveByQueryResult(AppSearchResult.RESULT_OK);
        verifyGetStorageInfoResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOn_allApis() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(1), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(
                DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSetSchema:5;globalGetSchema:5;localGetSchema:5;localGetNamespaces:5;"
                        + "localPutDocuments:5;globalGetDocuments:5;localGetDocuments:5;"
                        + "localSearch:5;globalSearch:5;globalGetNextPage:5;localGetNextPage:5;"
                        + "invalidateNextPageToken:5;localWriteSearchResultsToFile:5;"
                        + "localPutDocumentsFromFile:5;localSearchSuggestion:5;"
                        + "globalReportUsage:5;localReportUsage:5;localRemoveByDocumentId:5;"
                        + "localRemoveBySearch:5;localGetStorageInfo:5;flush:5;",
                false);

        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);
        verifySearchResult(RESULT_RATE_LIMITED);
        verifyPutDocumentsResult(RESULT_RATE_LIMITED);
        verifyLocalGetDocumentsResult(RESULT_RATE_LIMITED);
        verifyLocalGetNextPageResult(RESULT_RATE_LIMITED);
        verifyGlobalGetDocumentsResult(RESULT_RATE_LIMITED);
        verifyGlobalSearchResult(RESULT_RATE_LIMITED);
        verifyGlobalGetNextPageResult(RESULT_RATE_LIMITED);
        verifyInvalidateNextPageTokenResult(RESULT_RATE_LIMITED);
        verifyGlobalReportUsageResult(RESULT_RATE_LIMITED);
        verifyPersistToDiskResult(RESULT_RATE_LIMITED);

        // initialize, registerObserver and unregisterObserver do not have rate limit.
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOff_acceptTasksAndIgnoreCapacities()
            throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(false), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(10), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);

        // All rate limits should be ignored when rate limit is off
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOn_dropTaskDueToCapacitiesExceeded()
            throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(10), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);

        // Set Schema call is rejected because of per-package capacity exceeded
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        // Set Schema call is rejected because of total capacity exceeded
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);
        // Other calls should be fine
        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOn_noTasksDropped() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(1000), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);

        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOnToOff() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(10), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);
        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);

        // All calls should be fine after switching rate limiting to off
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(false), false);
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitOffToOn() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(false), false);
        verifyLocalCallsResults(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);

        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(5), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);
        // Some calls are rejected once rate limiting gets enabled
        verifySearchResult(RESULT_RATE_LIMITED);
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitChangeToHigherLimit() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(10), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:3;localSetSchema:9;localGetSchema:15",
                false);
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);

        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);

        // Only getSchema call should be rejected after setting to higher limit
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(15), false);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);

        verifySetSchemaResult(AppSearchResult.RESULT_OK);
        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testAppSearchRateLimit_rateLimitChangeToLowerLimit() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(10), false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.8f),
                false);
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_API_COSTS,
                "localSearch:6;localSetSchema:9;localGetSchema:15",
                false);
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);

        verifySearchResult(AppSearchResult.RESULT_OK);
        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);

        // Search call should also get rejected after setting a lower limit
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
                Float.toString(0.5f),
                false);
        verifySearchResult(RESULT_RATE_LIMITED);
        verifySetSchemaResult(RESULT_RATE_LIMITED);
        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);

        verifyPutDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyLocalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetDocumentsResult(AppSearchResult.RESULT_OK);
        verifyGlobalSearchResult(AppSearchResult.RESULT_OK);
        verifyGlobalGetNextPageResult(AppSearchResult.RESULT_OK);
        verifyInvalidateNextPageTokenResult(AppSearchResult.RESULT_OK);
        verifyGlobalReportUsageResult(AppSearchResult.RESULT_OK);
        verifyPersistToDiskResult(AppSearchResult.RESULT_OK);
        verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
        verifyInitializeResult(AppSearchResult.RESULT_OK);
        verifyGlobalCallsResults(AppSearchResult.RESULT_OK);
    }

    @Test
    public void testEnterpriseGetSchema_noEnterpriseUser_emptyResult() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                        BINDER_CALL_START_TIME, /* isForEnterprise= */ true),
                callback);
        AppSearchResult<GetSchemaResponse> result =
                (AppSearchResult<GetSchemaResponse>) callback.get();
        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        assertThat(result.getResultValue().getSchemas()).isEmpty();
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX)
    @Test
    public void testEnterpriseGetDocuments_noEnterpriseUser_emptyResult() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(
                                NAMESPACE)
                                .addIds(/* ids= */ Arrays.asList("123", "456", "789"))
                                .build(),
                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ true),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        assertThat(callback.getBatchResult().getAll()).isEmpty();
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX)
    @Test
    public void testEnterpriseGetDocuments_noEnterpriseUser_notFoundResults() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(
                                NAMESPACE)
                                .addIds(/* ids= */ Arrays.asList("123", "456", "789"))
                                .build(),
                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ true),
                callback);
        assertThat(callback.get()).isNull(); // null means there wasn't an error
        assertThat(callback.getBatchResult().getFailures()).containsExactly("123",
                AppSearchResult.newFailedResult(RESULT_NOT_FOUND,
                        "Document (namespace, 123) not found."), "456",
                AppSearchResult.newFailedResult(RESULT_NOT_FOUND,
                        "Document (namespace, 456) not found."), "789",
                AppSearchResult.newFailedResult(RESULT_NOT_FOUND,
                        "Document (namespace, 789) not found."));
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @Test
    public void testEnterpriseGlobalSearch_noEnterpriseUser_emptyResult() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ true), callback);
        AppSearchResult<SearchResultPage> result =
                (AppSearchResult<SearchResultPage>) callback.get();
        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        assertThat(result.getResultValue().getResults()).isEmpty();
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @Test
    public void testEnterpriseGetNextPage_noEnterpriseUser_emptyResult() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* databaseName= */ null,/* nextPageToken= */ 0,
                AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                BINDER_CALL_START_TIME, /* isForEnterprise= */ true), callback);
        AppSearchResult<SearchResultPage> result =
                (AppSearchResult<SearchResultPage>) callback.get();
        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
        assertThat(result.getResultValue().getResults()).isEmpty();
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @Test
    public void testEnterpriseInvalidateNextPageToken_noEnterpriseUser() throws Exception {
        // Even on devices with an enterprise user, this test will run properly, since we haven't
        // unlocked the enterprise user for our local instance of AppSearchManagerService
        mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ true));
        // No CallStats logged since we returned early
        verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
    }

    @Test
    public void testIsolatedStorageNotAvailable() throws Exception {
        Context context = ApplicationProvider.getApplicationContext();
        ServiceAppSearchConfig appSearchConfig =
                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR, context);
        DeviceConfig.setProperty(
                DeviceConfig.NAMESPACE_APPSEARCH,
                KEY_ISOLATED_STORAGE_DISABLED,
                Boolean.toString(false),
                false);
        assumeTrue(
                IsolatedStorageServiceManager.isIsolatedStorageAvailable(context, appSearchConfig));
        // The original user0 was created to not use isolated storage, and that result was
        // cached. Clear the cache so that our new settings take effect.
        AppSearchUserInstanceManager.getInstance()
                .closeAndRemoveUserInstance(mUserHandle, /* removeUserData= */ true);
        IsolatedStorageServiceManager isolatedStorageServiceManager =
                new IsolatedStorageServiceManager(
                        context,
                        appSearchConfig,
                        ExecutorManager.createDefaultScheduledExecutorService());
        // Ensure that AppSearch fails if the isolated storage service fails
        TestContext testContext =
                new TestContext(
                        context,
                        mRoleManager,
                        mDevicePolicyManager,
                        /* isIsolatedStorageAvailable= */ true);
        assertThrows(
                AppSearchException.class,
                () ->
                        AppSearchUserInstanceManager.getInstance()
                                .getOrCreateUserInstance(
                                        testContext,
                                        mUserHandle,
                                        appSearchConfig,
                                        mExecutorManager,
                                        isolatedStorageServiceManager));
    }

    @Test
    public void testPutDocuments_usesWriteExecutor() throws Exception {
        // Set up executor spies
        ExecutorManager executorManager = mAppSearchManagerService.getExecutorManager();
        ExecutorService writeExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ false));
        ExecutorService readExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ true));
        executorManager.setUserExecutorForTest(mUserHandle, writeExecutorSpy);
        executorManager.setReadOnlyUserExecutorForTest(mUserHandle, readExecutorSpy);

        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.putDocuments(
                new PutDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        new DocumentsParcel(Collections.emptyList(), Collections.emptyList()),
                        mUserHandle, BINDER_CALL_START_TIME), callback);

        // Verify write executor was used
        verify(writeExecutorSpy, times(1)).execute(any());
        verify(readExecutorSpy, never()).execute(any());
    }

    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEPARATE_READ_WRITE_EXECUTORS)
    @Test
    public void testSearch_usesReadOnlyExecutor() throws Exception {
        // Set up executor spies
        ExecutorManager executorManager = mAppSearchManagerService.getExecutorManager();
        ExecutorService writeExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ false));
        ExecutorService readExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ true));
        executorManager.setUserExecutorForTest(mUserHandle, writeExecutorSpy);
        executorManager.setReadOnlyUserExecutorForTest(mUserHandle, readExecutorSpy);

        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.search(
                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext,
                        mCallingPid),
                        DATABASE_NAME, /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                        BINDER_CALL_START_TIME), callback);

        // Verify read executor was used
        verify(writeExecutorSpy, never()).execute(any());
        verify(readExecutorSpy, times(1)).execute(any());
    }

    @Test
    public void testSetSchema_usesWriteExecutor() throws Exception {
        // Set up executor spies
        ExecutorManager executorManager = mAppSearchManagerService.getExecutorManager();
        ExecutorService writeExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ false));
        ExecutorService readExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ true));
        executorManager.setUserExecutorForTest(mUserHandle, writeExecutorSpy);
        executorManager.setReadOnlyUserExecutorForTest(mUserHandle, readExecutorSpy);

        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.setSchema(
                new SetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                        DATABASE_NAME,
                        /* schemaBundles= */ Collections.emptyList(),
                        /* visibilityBundles= */ Collections.emptyList(),
                        /* forceOverride= */ false, /* schemaVersion= */ 0, mUserHandle,
                        BINDER_CALL_START_TIME, SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE),
                callback);

        // Verify write executor was used
        verify(writeExecutorSpy, times(1)).execute(any());
        verify(readExecutorSpy, never()).execute(any());
    }

    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEPARATE_READ_WRITE_EXECUTORS)
    @Test
    public void testGetSchema_usesReadOnlyExecutor() throws Exception {
        // Set up executor spies
        ExecutorManager executorManager = mAppSearchManagerService.getExecutorManager();
        ExecutorService writeExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ false));
        ExecutorService readExecutorSpy =
                spy(
                        (ExecutorService)
                                executorManager.getOrCreateUserExecutor(
                                        mUserHandle, /* isReadOnly= */ true));
        executorManager.setUserExecutorForTest(mUserHandle, writeExecutorSpy);
        executorManager.setReadOnlyUserExecutorForTest(mUserHandle, readExecutorSpy);

        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                        BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);

        // Verify read executor was used
        verify(writeExecutorSpy, never()).execute(any());
        verify(readExecutorSpy, times(1)).execute(any());
    }

    private void verifyLocalCallsResults(int resultCode) throws Exception {
        // These APIs are local calls since they specify a database. If the API specifies a target
        // package, then the target package matches the calling package
        verifySetSchemaResult(resultCode);
        verifyLocalGetSchemaResult(resultCode);
        verifyGetNamespacesResult(resultCode);
        verifyPutDocumentsResult(resultCode);
        verifyLocalGetDocumentsResult(resultCode);
        verifySearchResult(resultCode);
        verifyLocalGetNextPageResult(resultCode);
        verifyWriteSearchResultsToFileResult(resultCode);
        verifyPutDocumentsFromFileResult(resultCode);
        verifySearchSuggestionResult(resultCode);
        verifyLocalReportUsageResult(resultCode);
        verifyRemoveByDocumentIdResult(resultCode);
        verifyRemoveByQueryResult(resultCode);
        verifyGetStorageInfoResult(resultCode);
    }

    private void verifyGlobalCallsResults(int resultCode) throws Exception {
        // These APIs are global calls since either they do not specify a database or if they do,
        // they specify the database along with a target package that does not match the calling
        // package
        verifyGlobalGetSchemaResult(resultCode);
        verifyGlobalGetDocumentsResult(resultCode);
        verifyGlobalSearchResult(resultCode);
        verifyGlobalGetNextPageResult(resultCode);
        verifyInvalidateNextPageTokenResult(resultCode);
        verifyGlobalReportUsageResult(resultCode);
        verifyPersistToDiskResult(resultCode);
        verifyRegisterObserverCallbackResult(resultCode);
        verifyUnregisterObserverCallbackResult(resultCode);
        verifyInitializeResult(resultCode);
    }

    private void verifySetSchemaResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.setSchema(
                new SetSchemaAidlRequest(
                AppSearchAttributionSource
                    .createAttributionSource(mContext, mCallingPid), DATABASE_NAME,
                /* schemaBundles= */ Collections.emptyList(),
                /* visibilityBundles= */ Collections.emptyList(), /* forceOverride= */ false,
                /* schemaVersion= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_SET_SCHEMA, callback.get());
    }

    private void verifyLocalGetSchemaResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                        BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_SCHEMA, callback.get());
    }

    private void verifyGlobalGetSchemaResult(int resultCode) throws Exception {
        String otherPackageName = mContext.getPackageName() + "foo";
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getSchema(
                new GetSchemaAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        otherPackageName, DATABASE_NAME, mUserHandle, BINDER_CALL_START_TIME,
                        /* isForEnterprise= */ false),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_GET_SCHEMA, callback.get());
    }

    private void verifyGetNamespacesResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNamespaces(
                new GetNamespacesAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_NAMESPACES, callback.get());
    }

    private void verifyPutDocumentsResult(int resultCode) throws Exception {
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.putDocuments(
                new PutDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        new DocumentsParcel(Collections.emptyList(), Collections.emptyList()),
                        mUserHandle, BINDER_CALL_START_TIME), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_PUT_DOCUMENTS, callback.get());
    }

    private void verifyLocalGetDocumentsResult(int resultCode) throws Exception {
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        mContext.getPackageName(), DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_DOCUMENTS, callback.get());
    }

    private void verifyGlobalGetDocumentsResult(int resultCode) throws Exception {
        String otherPackageName = mContext.getPackageName() + "foo";
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.getDocuments(
                new GetDocumentsAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        otherPackageName, DATABASE_NAME,
                        new GetByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID, callback.get());
    }

    private void verifySearchResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.search(
                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext,
                        mCallingPid),
                        DATABASE_NAME,/* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                        BINDER_CALL_START_TIME), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_SEARCH, callback.get());
    }

    private void verifyGlobalSearchResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ false), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_SEARCH, callback.get());
    }

    private void verifyLocalGetNextPageResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                DATABASE_NAME,
                /* nextPageToken= */ 0,
                AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_NEXT_PAGE, callback.get());
    }

    private void verifyGlobalGetNextPageResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* databaseName= */ null, /* nextPageToken= */ 0,
                AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_GET_NEXT_PAGE, callback.get());
    }

    private void verifyInvalidateNextPageTokenResult(int resultCode) throws Exception {
        mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                /* isForEnterprise= */ false));
        verifyCallResult(resultCode, CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, /* result= */
                null);
    }

    private void verifyWriteSearchResultsToFileResult(int resultCode) throws Exception {
        File tempFile = mTemporaryFolder.newFile();
        FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_WRONLY);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.writeSearchResultsToFile(
                new WriteSearchResultsToFileAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        new ParcelFileDescriptor(fd), /* searchExpression= */ "", EMPTY_SEARCH_SPEC,
                        mUserHandle, BINDER_CALL_START_TIME), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
                callback.get());
    }

    private void verifyPutDocumentsFromFileResult(int resultCode) throws Exception {
        File tempFile = mTemporaryFolder.newFile();
        FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_RDONLY);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.putDocumentsFromFile(new PutDocumentsFromFileAidlRequest(
                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                DATABASE_NAME,
                new ParcelFileDescriptor(fd), mUserHandle,
                new SchemaMigrationStats.Builder(mContext.getPackageName(), DATABASE_NAME).build(),
                /* totalLatencyStartTimeMillis= */ 0, BINDER_CALL_START_TIME), callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE, callback.get());
    }

    private void verifySearchSuggestionResult(int resultCode) throws Exception {
        SearchSuggestionSpec searchSuggestionSpec =
            new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1).build();
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.searchSuggestion(
                new SearchSuggestionAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        DATABASE_NAME, /* suggestionQueryExpression= */ "foo", searchSuggestionSpec,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_SEARCH_SUGGESTION, callback.get());
    }

    private void verifyLocalReportUsageResult(int resultCode) throws Exception {
        setUpTestSchema(mContext.getPackageName(), DATABASE_NAME);
        setUpTestDocument(mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID);
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.reportUsage(
                new ReportUsageAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                mContext.getPackageName(), DATABASE_NAME,
                        new ReportUsageRequest.Builder(NAMESPACE, ID)
                                .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
                                .build(),
                        /* systemUsage= */ false, mUserHandle, BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_REPORT_USAGE, callback.get());
        removeTestSchema(mContext.getPackageName(), DATABASE_NAME);
    }

    private void verifyGlobalReportUsageResult(int resultCode) throws Exception {
        // Grant system access for global report usage
        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA);
        try {
            String otherPackageName = mContext.getPackageName() + "foo";
            setUpTestSchema(otherPackageName, DATABASE_NAME);
            setUpTestDocument(otherPackageName, DATABASE_NAME, NAMESPACE, ID);
            TestResultCallback callback = new TestResultCallback();
            mAppSearchManagerServiceStub.reportUsage(
                    new ReportUsageAidlRequest(
                            AppSearchAttributionSource.createAttributionSource(mContext,
                                    mCallingPid),
                            otherPackageName, DATABASE_NAME,
                            new ReportUsageRequest.Builder(NAMESPACE, ID)
                                    .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
                                    .build(),
                    /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME),
                    callback);
            verifyCallResult(resultCode, CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE, callback.get());
            removeTestSchema(otherPackageName, DATABASE_NAME);
        } finally {
            mUiAutomation.dropShellPermissionIdentity();
        }
    }

    private void verifyRemoveByDocumentIdResult(int resultCode) throws Exception {
        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
        mAppSearchManagerServiceStub.removeByDocumentId(
                new RemoveByDocumentIdAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid),
                        DATABASE_NAME,
                        new RemoveByDocumentIdRequest.Builder(NAMESPACE)
                                .addIds(/* ids= */ Collections.emptyList())
                                .build(),
                        mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, callback.get());
    }

    private void verifyRemoveByQueryResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.removeByQuery(
                new RemoveByQueryAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        /* queryExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
                callback.get());
    }

    private void verifyGetStorageInfoResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.getStorageInfo(
                new GetStorageInfoAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), DATABASE_NAME,
                        mUserHandle, BINDER_CALL_START_TIME),
                callback);
        verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_STORAGE_INFO, callback.get());
    }

    private void verifyPersistToDiskResult(int resultCode) throws Exception {
        if (!Flags.enableNoOpManualPersist()) {
            mAppSearchManagerServiceStub.persistToDisk(
                    new PersistToDiskAidlRequest(
                            AppSearchAttributionSource.createAttributionSource(mContext,
                                    mCallingPid), mUserHandle,
                            BINDER_CALL_START_TIME));
            verifyCallResult(resultCode, CallStats.CALL_TYPE_FLUSH, /* result= */ null);
        }
    }

    private void verifyRegisterObserverCallbackResult(int resultCode) throws Exception {
        AppSearchResultParcel<Void> resultParcel =
                mAppSearchManagerServiceStub.registerObserverCallback(
                        new RegisterObserverCallbackAidlRequest(
                                AppSearchAttributionSource.createAttributionSource(mContext,
                                        mCallingPid),
                                mContext.getPackageName(),
                                new ObserverSpec.Builder().build(),
                                mUserHandle,
                                BINDER_CALL_START_TIME),
                        new IAppSearchObserverProxy.Stub() {
                            @Override
                            public void onSchemaChanged(String packageName, String databaseName,
                                    List<String> changedSchemaNames) throws RemoteException {
                            }

                            @Override
                            public void onDocumentChanged(String packageName, String databaseName,
                                    String namespace, String schemaName,
                                    List<String> changedDocumentIds) throws RemoteException {
                            }
                        });
        verifyCallResult(resultCode, CallStats.CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
                resultParcel.getResult());
    }


    private void verifyUnregisterObserverCallbackResult(int resultCode) throws Exception {
        AppSearchResultParcel<Void> resultParcel =
                mAppSearchManagerServiceStub.unregisterObserverCallback(
                        new UnregisterObserverCallbackAidlRequest(
                                AppSearchAttributionSource.createAttributionSource(mContext,
                                        mCallingPid),
                                mContext.getPackageName(), mUserHandle,
                                BINDER_CALL_START_TIME),
                        new IAppSearchObserverProxy.Stub() {
                            @Override
                            public void onSchemaChanged(String packageName, String databaseName,
                                    List<String> changedSchemaNames) throws RemoteException {
                            }

                            @Override
                            public void onDocumentChanged(String packageName, String databaseName,
                                    String namespace, String schemaName,
                                    List<String> changedDocumentIds) throws RemoteException {
                            }
                        });
        verifyCallResult(resultCode, CallStats.CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
                resultParcel.getResult());
    }

    private void verifyInitializeResult(int resultCode) throws Exception {
        TestResultCallback callback = new TestResultCallback();
        mAppSearchManagerServiceStub.initialize(
                new InitializeAidlRequest(
                        AppSearchAttributionSource.createAttributionSource(mContext,
                                mCallingPid), mUserHandle,
                        BINDER_CALL_START_TIME),
                callback);
        if (resultCode == RESULT_DENIED) {
            verify(mLogger, never()).logStats(any(CallStats.class));
        } else {
            verifyCallResult(resultCode, CallStats.CALL_TYPE_INITIALIZE, /* result= */ null);
        }
        assertThat(callback.get().getResultCode()).isEqualTo(resultCode);
    }

    private void verifyCallResult(int resultCode, int callType, AppSearchResult<?> result) {
        ArgumentCaptor<CallStats> captor = ArgumentCaptor.forClass(CallStats.class);
        verify(mLogger, timeout(1000).times(1)).logStats(captor.capture());
        assertThat(captor.getValue().getCallType()).isEqualTo(callType);
        assertThat(captor.getValue().getStatusCode()).isEqualTo(resultCode);
        assertThat(captor.getValue().getEstimatedBinderLatencyMillis()).isGreaterThan(0);
        clearInvocations(mLogger);
        // Not all calls return a result
        if (result != null) {
            assertThat(result.getResultCode()).isEqualTo(resultCode);
        }
    }

    private void setUpTestSchema(String packageName, String databaseName) throws Exception {
        // Insert schema
        List<AppSearchSchema> schemas = Collections.singletonList(
                new AppSearchSchema.Builder("type").build());
        InternalSetSchemaResponse internalSetSchemaResponse =
                mUserInstance
                        .getAppSearchImpl()
                        .setSchema(
                                packageName,
                                databaseName,
                                schemas,
                                /* visibilityDocuments= */ Collections.emptyList(),
                                /* forceOverride= */ false,
                                /* version= */ 0,
                                /* setSchemaStatsBuilder= */ null,
                                /* callStatsBuilder= */ null);
        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
    }

    private void setUpTestDocument(String packageName, String databaseName, String namespace,
            String id) throws Exception {
        // Insert a document
        GenericDocument document = new GenericDocument.Builder<>(namespace, id, "type").build();
        mUserInstance
                .getAppSearchImpl()
                .putDocument(
                        packageName,
                        databaseName,
                        document,
                        /* sendChangeNotifications= */ false,
                        /* logger= */ null,
                        /* callStatsBuilder= */ null);
    }

    private void removeTestSchema(String packageName, String databaseName) throws Exception {
        InternalSetSchemaResponse internalSetSchemaResponse =
                mUserInstance
                        .getAppSearchImpl()
                        .setSchema(
                                packageName,
                                databaseName,
                                /* schemas= */ Collections.emptyList(),
                                /* visibilityDocuments= */ Collections.emptyList(),
                                /* forceOverride= */ true,
                                /* version= */ 0,
                                /* setSchemaStatsBuilder= */ null,
                                /* callStatsBuilder= */ null);
        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
    }

    private static final class TestContext extends ContextWrapper {
        private final RoleManager mRoleManager;
        private final DevicePolicyManager mDevicePolicyManager;
        private final boolean isIsolatedStorageAvailable;

        @Nullable private PackageManager mPackageManager;
        @Nullable private String mPackageName;

        TestContext(
                Context base,
                RoleManager roleManager,
                DevicePolicyManager devicePolicyManager,
                boolean isIsolatedStorageAvailable) {
            super(base);
            mRoleManager = roleManager;
            mDevicePolicyManager = devicePolicyManager;
            this.isIsolatedStorageAvailable = isIsolatedStorageAvailable;
        }

        @Override
        public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
                @NonNull IntentFilter filter, @Nullable String broadcastPermission,
                @Nullable Handler scheduler) {
            // Do nothing
            return null;
        }

        @Override
        public Context createContextAsUser(UserHandle user, int flags) {
            return this;
        }

        @Override
        public PackageManager getPackageManager() {
            if (mPackageManager != null) {
                return mPackageManager;
            }
            return super.getPackageManager();
        }

        @Override
        public String getPackageName() {
            if (mPackageName != null) {
                return mPackageName;
            }
            return super.getPackageName();
        }

        @Override
        public AttributionSource getAttributionSource() {
            if (mPackageName != null) {
                return super.getAttributionSource().withPackageName(mPackageName);
            }
            return super.getAttributionSource();
        }

        @Nullable
        @Override
        public Object getSystemService(String name) {
            if (Context.ROLE_SERVICE.equals(name)) {
                return mRoleManager;
            }
            if (Context.DEVICE_POLICY_SERVICE.equals(name)) {
                return mDevicePolicyManager;
            }
            /* TODO (b/399479359)
             * Force use of native icing for AppSearchManagerServiceTests, which mocks
             * servives and does have the isolated storage service
             */
            if (Context.VIRTUALIZATION_SERVICE.equals(name) && !isIsolatedStorageAvailable) {
                return null;
            }
            return super.getSystemService(name);
        }
    };

    private static final class TestResultCallback extends IAppSearchResultCallback.Stub {
        private final SettableFuture<AppSearchResult<?>> future = SettableFuture.create();

        @Override
        public void onResult(AppSearchResultParcel appSearchResultParcel) {
            future.set(appSearchResultParcel.getResult());
        }

        @Override
        public void onResultV2(AppSearchResultParcelV2 appSearchResultParcel) {
            future.set(appSearchResultParcel.getResult());
        }

        public AppSearchResult<?> get() throws InterruptedException, ExecutionException {
            return future.get();
        }
    }

    private static final class TestBatchResultErrorCallback extends
            IAppSearchBatchResultCallback.Stub {
        private final SettableFuture<AppSearchResult<?>> future = SettableFuture.create();
        private final SettableFuture<AppSearchBatchResult<?, ?>> batchFuture =
                SettableFuture.create();

        @Override
        public void onResult(AppSearchBatchResultParcel appSearchBatchResultParcel) {
            future.set(null);
            batchFuture.set(appSearchBatchResultParcel.getResult());
        }

        @Override
        public void onSystemError(AppSearchResultParcel appSearchResultParcel) {
            future.set(appSearchResultParcel.getResult());
            batchFuture.set(null);
        }

        public AppSearchResult<?> get() throws InterruptedException, ExecutionException {
            return future.get();
        }

        public AppSearchBatchResult<?, ?> getBatchResult()
                throws InterruptedException, ExecutionException {
            return batchFuture.get();
        }
    }
}
