Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9b26c3c9 authored by Alexander Dorokhine's avatar Alexander Dorokhine
Browse files

Guard system usage reporting by checking the caller has system access.

Adding this check in a testable way requires refactoring VisibilityStore
to pull it out of management by the AppSearchImpl object. It is now
initialized as a sibling of AppSearchImpl, managed by
AppSearchUserInstanceManager.

This has several benefits:
* Breaks the complicated initialization inter-dependency between
  AppSearchImpl and VisibilityStore
* Reduces duplicative singleton managers
* Allows AppSearchImpl to be tested more easily by accepting a
  "hasSystemSurfaceable" boolean that can be set in tests
* Reduces the number of times we have to call into VisStore; we can
  determine whether the caller has system access in advance instead of
  repeating the check for every schema type

Bug: 180058203
Bug: 183031844
Test: GlobalSearchSessionCtsTest#testReportSystemUsage_ForbiddenFromNonSystem
Test: GlobalSearchSessionPlatformCtsTest#testReportSystemUsage
Test: VisibilityStoreTest

Change-Id: I84c84819f287628ccf8af369f5481a8e90255f62
parent 5c86c0c1
Loading
Loading
Loading
Loading
+149 −151

File changed.

Preview size limit exceeded, changes collapsed.

+58 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.annotation.NonNull;

import com.android.server.appsearch.external.localstorage.AppSearchImpl;
import com.android.server.appsearch.stats.PlatformLogger;
import com.android.server.appsearch.visibilitystore.VisibilityStore;

import java.util.Objects;

/**
 * Container for AppSearch classes that should only be initialized once per device-user and make up
 * the core of the AppSearch system.
 */
public final class AppSearchUserInstance {
    private final PlatformLogger mLogger;
    private final AppSearchImpl mAppSearchImpl;
    private final VisibilityStore mVisibilityStore;

    AppSearchUserInstance(
            @NonNull PlatformLogger logger,
            @NonNull AppSearchImpl appSearchImpl,
            @NonNull VisibilityStore visibilityStore) {
        mLogger = Objects.requireNonNull(logger);
        mAppSearchImpl = Objects.requireNonNull(appSearchImpl);
        mVisibilityStore = Objects.requireNonNull(visibilityStore);
    }

    @NonNull
    public PlatformLogger getLogger() {
        return mLogger;
    }

    @NonNull
    public AppSearchImpl getAppSearchImpl() {
        return mAppSearchImpl;
    }

    @NonNull
    public VisibilityStore getVisibilityStore() {
        return mVisibilityStore;
    }
}
+195 −0
Original line number Diff line number Diff line
@@ -16,55 +16,58 @@

package com.android.server.appsearch;


import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.server.appsearch.external.localstorage.AppSearchImpl;
import com.android.server.appsearch.external.localstorage.AppSearchLogger;
import com.android.server.appsearch.external.localstorage.FrameworkOptimizeStrategy;
import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
import com.android.server.appsearch.stats.PlatformLogger;
import com.android.server.appsearch.visibilitystore.VisibilityStore;

import java.io.File;
import java.util.Map;
import java.util.Objects;

/**
 * Manages the lifecycle of instances of {@link AppSearchImpl}.
 * Manages the lifecycle of AppSearch classes that should only be initialized once per device-user
 * and make up the core of the AppSearch system.
 *
 * <p>These instances are managed per unique device-user.
 * @hide
 */
public final class ImplInstanceManager {
    private static final String TAG = "AppSearchImplInstanceMa";
public final class AppSearchUserInstanceManager {
    private static final String TAG = "AppSearchUserInstanceMa";

    private static ImplInstanceManager sImplInstanceManager;
    private static volatile AppSearchUserInstanceManager sAppSearchUserInstanceManager;

    @GuardedBy("mInstancesLocked")
    private final Map<UserHandle, AppSearchImpl> mInstancesLocked = new ArrayMap<>();
    private final Map<UserHandle, AppSearchUserInstance> mInstancesLocked = new ArrayMap<>();

    private AppSearchUserInstanceManager() {}

    /**
     * Gets an instance of ImplInstanceManager to be used.
     * Gets an instance of AppSearchUserInstanceManager to be used.
     *
     * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
     * existing instance will be returned.
     */
    @NonNull
    public static ImplInstanceManager getInstance(@NonNull Context context) {
        if (sImplInstanceManager == null) {
            synchronized (ImplInstanceManager.class) {
                if (sImplInstanceManager == null) {
                    sImplInstanceManager = new ImplInstanceManager();
    public static AppSearchUserInstanceManager getInstance() {
        if (sAppSearchUserInstanceManager == null) {
            synchronized (AppSearchUserInstanceManager.class) {
                if (sAppSearchUserInstanceManager == null) {
                    sAppSearchUserInstanceManager = new AppSearchUserInstanceManager();
                }
            }
        }
        return sImplInstanceManager;
        return sAppSearchUserInstanceManager;
    }

    /**
@@ -77,32 +80,35 @@ public final class ImplInstanceManager {
        // TODO(b/191059409): Unhide Environment#getDataSystemCeDirectory and switch to it.
        File systemCeDir = new File(Environment.getDataDirectory(), "system_ce");
        File systemCeUserDir = new File(systemCeDir, String.valueOf(userHandle.getIdentifier()));
        return new File(systemCeUserDir, "appSearch");
        return new File(systemCeUserDir, "appsearch");
    }

    /**
     * Gets an instance of AppSearchImpl for the given user, or creates one if none exists.
     * Gets an instance of AppSearchUserInstance for the given user, or creates one if none exists.
     *
     * <p>If no AppSearchImpl instance exists for the unlocked user, Icing will be initialized and
     * <p>If no AppSearchUserInstance exists for the unlocked user, Icing will be initialized and
     * one will be created.
     *
     * @param context The system context
     * @param context The context
     * @param userHandle The multi-user handle of the device user calling AppSearch
     * @return An initialized {@link AppSearchImpl} for this user
     * @param config Flag manager for AppSearch
     * @return An initialized {@link AppSearchUserInstance} for this user
     */
    @NonNull
    public AppSearchImpl getOrCreateAppSearchImpl(
    public AppSearchUserInstance getOrCreateUserInstance(
            @NonNull Context context,
            @NonNull UserHandle userHandle,
            @Nullable AppSearchLogger logger) throws AppSearchException {
            @NonNull AppSearchConfig config)
            throws AppSearchException {
        Objects.requireNonNull(context);
        Objects.requireNonNull(userHandle);
        Objects.requireNonNull(config);

        synchronized (mInstancesLocked) {
            AppSearchImpl instance = mInstancesLocked.get(userHandle);
            AppSearchUserInstance instance = mInstancesLocked.get(userHandle);
            if (instance == null) {
                Context userContext = context.createContextAsUser(userHandle, /*flags=*/ 0);
                instance = createImpl(userContext, userHandle, logger);
                instance = createUserInstance(userContext, userHandle, config);
                mInstancesLocked.put(userHandle, instance);
            }
            return instance;
@@ -110,61 +116,80 @@ public final class ImplInstanceManager {
    }

    /**
     * Close and remove an instance of {@link AppSearchImpl} for the given user.
     * Closes and removes an {@link AppSearchUserInstance} for the given user.
     *
     * <p>All mutation apply to this {@link AppSearchImpl} will be persisted to disk.
     * <p>All mutations applied to the underlying {@link AppSearchImpl} will be persisted to disk.
     *
     * @param userHandle The multi-user user handle of the user that need to be removed.
     */
    public void closeAndRemoveAppSearchImplForUser(@NonNull UserHandle userHandle) {
    public void closeAndRemoveUserInstance(@NonNull UserHandle userHandle) {
        Objects.requireNonNull(userHandle);
        synchronized (mInstancesLocked) {
            AppSearchImpl appSearchImpl = mInstancesLocked.get(userHandle);
            if (appSearchImpl != null) {
                appSearchImpl.close();
                mInstancesLocked.remove(userHandle);
            AppSearchUserInstance instance = mInstancesLocked.remove(userHandle);
            if (instance != null) {
                instance.getAppSearchImpl().close();
            }
        }
    }

    /**
     * Gets an instance of AppSearchImpl for the given user.
     * Gets an {@link AppSearchUserInstance} for the given user.
     *
     * <p>This method should only be called by an initialized SearchSession, which has been already
     * created the AppSearchImpl instance for the given user.
     * <p>This method should only be called by an initialized SearchSession, which has already
     * called {@link #getOrCreateUserInstance} before.
     *
     * @param userHandle The multi-user handle of the device user calling AppSearch
     * @return An initialized {@link AppSearchImpl} for this user
     * @throws IllegalStateException if {@link AppSearchImpl} haven't created for the given user.
     * @return An initialized {@link AppSearchUserInstance} for this user
     * @throws IllegalStateException if {@link AppSearchUserInstance} haven't created for the given
     *                               user.
     */
    @NonNull
    public AppSearchImpl getAppSearchImpl(@NonNull UserHandle userHandle) {
    public AppSearchUserInstance getUserInstance(@NonNull UserHandle userHandle) {
        Objects.requireNonNull(userHandle);
        synchronized (mInstancesLocked) {
            AppSearchImpl instance = mInstancesLocked.get(userHandle);
            AppSearchUserInstance instance = mInstancesLocked.get(userHandle);
            if (instance == null) {
                // Impossible scenario, user cannot call an uninitialized SearchSession,
                // getInstance should always find the instance for the given user and never try to
                // create an instance for this user again.
                throw new IllegalStateException(
                        "AppSearchImpl has never been created for: " + userHandle);
                        "AppSearchUserInstance has never been created for: " + userHandle);
            }
            return instance;
        }
    }

    private AppSearchImpl createImpl(
    @NonNull
    private AppSearchUserInstance createUserInstance(
            @NonNull Context userContext,
            @NonNull UserHandle userHandle,
            @Nullable AppSearchLogger logger)
            @NonNull AppSearchConfig config)
            throws AppSearchException {
        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();

        // Initialize the classes that make up AppSearchUserInstance
        PlatformLogger logger = new PlatformLogger(userContext, userHandle, config);

        File appSearchDir = getAppSearchDir(userHandle);
        File icingDir = new File(appSearchDir, "icing");
        Log.i(TAG, "Creating new AppSearch instance at: " + icingDir);
        return AppSearchImpl.create(
                icingDir,
                userContext,
                /*logger=*/ null,
                new FrameworkOptimizeStrategy());
        AppSearchImpl appSearchImpl =
                AppSearchImpl.create(icingDir, initStatsBuilder, new FrameworkOptimizeStrategy());

        long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime();
        VisibilityStore visibilityStore = VisibilityStore.create(appSearchImpl, userContext);
        long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime();

        initStatsBuilder
                .setTotalLatencyMillis(
                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
                .setPrepareVisibilityStoreLatencyMillis(
                        (int)
                                (prepareVisibilityStoreLatencyEndMillis
                                        - prepareVisibilityStoreLatencyStartMillis));
        logger.logStats(initStatsBuilder.build());

        return new AppSearchUserInstance(logger, appSearchImpl, visibilityStore);
    }
}
+40 −92
Original line number Diff line number Diff line
@@ -39,7 +39,6 @@ import android.app.appsearch.SetSchemaResponse;
import android.app.appsearch.StorageInfo;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.util.LogUtil;
import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.ArrayMap;
@@ -157,9 +156,6 @@ public final class AppSearchImpl implements Closeable {
    @VisibleForTesting
    final IcingSearchEngine mIcingSearchEngineLocked;

    @GuardedBy("mReadWriteLock")
    private final VisibilityStore mVisibilityStoreLocked;

    // This map contains schema types and SchemaTypeConfigProtos for all package-database
    // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
    // prefixed schema type to its respective SchemaTypeConfigProto.
@@ -195,56 +191,27 @@ public final class AppSearchImpl implements Closeable {
     * <p>Instead, logger instance needs to be passed to each individual method, like create, query
     * and putDocument.
     *
     * @param logger collects stats for initialization if provided.
     * @param initStatsBuilder collects stats for initialization if provided.
     */
    @NonNull
    public static AppSearchImpl create(
            @NonNull File icingDir,
            @NonNull Context userContext,
            @Nullable AppSearchLogger logger,
            @Nullable InitializeStats.Builder initStatsBuilder,
            @NonNull OptimizeStrategy optimizeStrategy)
            throws AppSearchException {
        Objects.requireNonNull(icingDir);
        Objects.requireNonNull(userContext);
        Objects.requireNonNull(optimizeStrategy);

        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
        InitializeStats.Builder initStatsBuilder = null;
        if (logger != null) {
            initStatsBuilder = new InitializeStats.Builder();
        }

        AppSearchImpl appSearchImpl =
                new AppSearchImpl(
                        icingDir, userContext, initStatsBuilder, optimizeStrategy);

        long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime();
        appSearchImpl.initializeVisibilityStore();
        long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime();

        if (logger != null) {
            initStatsBuilder
                    .setTotalLatencyMillis(
                            (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
                    .setPrepareVisibilityStoreLatencyMillis(
                            (int)
                                    (prepareVisibilityStoreLatencyEndMillis
                                            - prepareVisibilityStoreLatencyStartMillis));
            logger.logStats(initStatsBuilder.build());
        }

        return appSearchImpl;
        return new AppSearchImpl(icingDir, initStatsBuilder, optimizeStrategy);
    }

    /** @param initStatsBuilder collects stats for initialization if provided. */
    private AppSearchImpl(
            @NonNull File icingDir,
            @NonNull Context userContext,
            @Nullable InitializeStats.Builder initStatsBuilder,
            @NonNull OptimizeStrategy optimizeStrategy)
            throws AppSearchException {
        mReadWriteLock.writeLock().lock();
        Objects.requireNonNull(icingDir);
        mOptimizeStrategy = Objects.requireNonNull(optimizeStrategy);

        mReadWriteLock.writeLock().lock();
        try {
            // We synchronize here because we don't want to call IcingSearchEngine.initialize() more
            // than once. It's unnecessary and can be a costly operation.
@@ -258,9 +225,6 @@ public final class AppSearchImpl implements Closeable {
                    "Constructing IcingSearchEngine, response",
                    Objects.hashCode(mIcingSearchEngineLocked));

            mVisibilityStoreLocked = new VisibilityStore(this, userContext);
            mOptimizeStrategy = optimizeStrategy;

            // The core initialization procedure. If any part of this fails, we bail into
            // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up).
            try {
@@ -342,23 +306,6 @@ public final class AppSearchImpl implements Closeable {
        }
    }

    /**
     * Initialize the visibility store in AppSearchImpl.
     *
     * @throws AppSearchException on IcingSearchEngine error.
     */
    void initializeVisibilityStore() throws AppSearchException {
        mReadWriteLock.writeLock().lock();
        try {
            throwIfClosedLocked();
            mLogUtil.piiTrace("Initializing VisibilityStore, request");
            mVisibilityStoreLocked.initialize();
            mLogUtil.piiTrace("Initializing VisibilityStore, response");
        } finally {
            mReadWriteLock.writeLock().unlock();
        }
    }

    @GuardedBy("mReadWriteLock")
    private void throwIfClosedLocked() {
        if (mClosedLocked) {
@@ -399,6 +346,8 @@ public final class AppSearchImpl implements Closeable {
     * @param packageName The package name that owns the schemas.
     * @param databaseName The name of the database where this schema lives.
     * @param schemas Schemas to set for this app.
     * @param visibilityStore If set, {@code schemasNotPlatformSurfaceable} and {@code
     *     schemasPackageAccessible} will be saved here if the schema is successfully applied.
     * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform
     *     surfaces.
     * @param schemasPackageAccessible Schema types that are visible to the specified packages.
@@ -416,6 +365,7 @@ public final class AppSearchImpl implements Closeable {
            @NonNull String packageName,
            @NonNull String databaseName,
            @NonNull List<AppSearchSchema> schemas,
            @Nullable VisibilityStore visibilityStore,
            @NonNull List<String> schemasNotPlatformSurfaceable,
            @NonNull Map<String, List<PackageIdentifier>> schemasPackageAccessible,
            boolean forceOverride,
@@ -479,6 +429,7 @@ public final class AppSearchImpl implements Closeable {
                removeFromMap(mSchemaMapLocked, prefix, schemaType);
            }

            if (visibilityStore != null) {
                Set<String> prefixedSchemasNotPlatformSurfaceable =
                        new ArraySet<>(schemasNotPlatformSurfaceable.size());
                for (int i = 0; i < schemasNotPlatformSurfaceable.size(); i++) {
@@ -493,11 +444,12 @@ public final class AppSearchImpl implements Closeable {
                    prefixedSchemasPackageAccessible.put(prefix + entry.getKey(), entry.getValue());
                }

            mVisibilityStoreLocked.setVisibility(
                visibilityStore.setVisibility(
                        packageName,
                        databaseName,
                        prefixedSchemasNotPlatformSurfaceable,
                        prefixedSchemasPackageAccessible);
            }

            return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
                    setSchemaResultProto, prefix);
@@ -817,7 +769,11 @@ public final class AppSearchImpl implements Closeable {
     * @param searchSpec Spec for setting filters, raw query etc.
     * @param callerPackageName Package name of the caller, should belong to the {@code
     *     userContext}.
     * @param visibilityStore Optional visibility store to obtain system and package visibility
     *     settings from
     * @param callerUid UID of the client making the globalQuery call.
     * @param callerHasSystemAccess Whether the caller has been positively identified as having
     *     access to schemas marked system surfaceable.
     * @param logger logger to collect globalQuery stats
     * @return The results of performing this search. It may contain an empty list of results if no
     *     documents matched the query.
@@ -828,7 +784,9 @@ public final class AppSearchImpl implements Closeable {
            @NonNull String queryExpression,
            @NonNull SearchSpec searchSpec,
            @NonNull String callerPackageName,
            @Nullable VisibilityStore visibilityStore,
            int callerUid,
            boolean callerHasSystemAccess,
            @Nullable AppSearchLogger logger)
            throws AppSearchException {
        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
@@ -885,15 +843,18 @@ public final class AppSearchImpl implements Closeable {
                if (packageName.equals(callerPackageName)) {
                    // Callers can always retrieve their own data
                    allow = true;
                } else if (visibilityStore == null) {
                    // If there's no visibility store, there's no extra access
                    allow = false;
                } else {
                    String databaseName = getDatabaseName(prefixedSchema);
                    allow =
                            mVisibilityStoreLocked.isSchemaSearchableByCaller(
                            visibilityStore.isSchemaSearchableByCaller(
                                    packageName,
                                    databaseName,
                                    prefixedSchema,
                                    callerPackageName,
                                    callerUid);
                                    callerUid,
                                    callerHasSystemAccess);
                }

                if (!allow) {
@@ -1488,9 +1449,6 @@ public final class AppSearchImpl implements Closeable {
    /**
     * Clears documents and schema across all packages and databaseNames.
     *
     * <p>This method also clear all data in {@link VisibilityStore}, an {@link
     * #initializeVisibilityStore()} must be called after this.
     *
     * <p>This method belongs to mutate group.
     *
     * @throws AppSearchException on IcingSearchEngine error.
@@ -1514,9 +1472,6 @@ public final class AppSearchImpl implements Closeable {
                    .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus()));
        }

        // Must be called after everything else since VisibilityStore may repopulate
        // IcingSearchEngine with an initial schema.
        mVisibilityStoreLocked.handleReset();
        checkSuccess(resetResultProto.getStatus());
    }

@@ -2075,13 +2030,6 @@ public final class AppSearchImpl implements Closeable {
        return result;
    }

    @GuardedBy("mReadWriteLock")
    @NonNull
    @VisibleForTesting
    VisibilityStore getVisibilityStoreLocked() {
        return mVisibilityStoreLocked;
    }

    /**
     * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums.
     *
+0 −126
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.stats;

import android.annotation.NonNull;
import android.content.Context;
import android.os.UserHandle;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.server.appsearch.AppSearchConfig;
import com.android.server.appsearch.AppSearchManagerService;

import java.util.Map;
import java.util.Objects;

/**
 * Manages the lifecycle of instances of {@link PlatformLogger}.
 *
 * <p>These instances are managed per unique device-user.
 */
public final class LoggerInstanceManager {
    private static volatile LoggerInstanceManager sLoggerInstanceManager;

    @GuardedBy("mInstancesLocked")
    private final Map<UserHandle, PlatformLogger> mInstancesLocked = new ArrayMap<>();

    private LoggerInstanceManager() {
    }

    /**
     * Gets an instance of {@link LoggerInstanceManager} to be used.
     *
     * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
     * existing instance will be returned.
     */
    @NonNull
    public static LoggerInstanceManager getInstance() {
        if (sLoggerInstanceManager == null) {
            synchronized (LoggerInstanceManager.class) {
                if (sLoggerInstanceManager == null) {
                    sLoggerInstanceManager =
                            new LoggerInstanceManager();
                }
            }
        }
        return sLoggerInstanceManager;
    }

    /**
     * Gets an instance of PlatformLogger for the given user, or creates one if none exists.
     *
     * @param context    The context
     * @param userHandle The multi-user handle of the device user calling AppSearch
     * @return An initialized {@link PlatformLogger} for this user
     */
    @NonNull
    public PlatformLogger getOrCreatePlatformLogger(
            @NonNull Context context, @NonNull UserHandle userHandle,
            @NonNull AppSearchConfig config) {
        Objects.requireNonNull(userHandle);
        synchronized (mInstancesLocked) {
            PlatformLogger instance = mInstancesLocked.get(userHandle);
            if (instance == null) {
                instance = new PlatformLogger(context, userHandle, config);
                mInstancesLocked.put(userHandle, instance);
            }
            return instance;
        }
    }

    /**
     * Gets an instance of PlatformLogger for the given user.
     *
     * <p>This method should only be called by an initialized SearchSession, which has been already
     * created the PlatformLogger instance for the given user.
     *
     * @param userHandle The multi-user handle of the device user calling AppSearch
     * @return An initialized {@link PlatformLogger} for this user
     * @throws IllegalStateException if {@link PlatformLogger} haven't created for the given user.
     */
    @NonNull
    public PlatformLogger getPlatformLogger(@NonNull UserHandle userHandle) {
        Objects.requireNonNull(userHandle);
        synchronized (mInstancesLocked) {
            PlatformLogger instance = mInstancesLocked.get(userHandle);
            if (instance == null) {
                // Impossible scenario, user cannot call an uninitialized SearchSession,
                // getInstance should always find the instance for the given user and never try to
                // create an instance for this user again.
                throw new IllegalStateException(
                        "PlatformLogger has never been created for: " + userHandle);
            }
            return instance;
        }
    }

    /**
     * Remove an instance of {@link PlatformLogger} for the given user.
     *
     * <p>This method should only be called if {@link AppSearchManagerService} receives an
     * ACTION_USER_REMOVED, which the logger instance of given user should be removed.
     *
     * @param userHandle The multi-user handle of the user that need to be removed.
     */
    public void removePlatformLoggerForUser(@NonNull UserHandle userHandle) {
        Objects.requireNonNull(userHandle);
        synchronized (mInstancesLocked) {
            mInstancesLocked.remove(userHandle);
        }
    }
}
Loading