/*
 * Copyright (C) 2020 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 android.app.appsearch;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
import android.app.appsearch.aidl.AppSearchAttributionSource;
import android.app.appsearch.aidl.IAppSearchManager;
import android.content.Context;
import android.os.Process;

import com.android.appsearch.flags.Flags;
import com.android.internal.util.Preconditions;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Provides access to the centralized AppSearch index maintained by the system.
 *
 * <p>AppSearch is an offline, on-device search library for managing structured data featuring:
 *
 * <ul>
 *   <li>APIs to index and retrieve data via full-text search.
 *   <li>An API for applications to explicitly grant read-access permission of their data to other
 *       applications. <b>See: {@link
 *       SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}</b>
 *   <li>An API for applications to opt into or out of having their data displayed on System UI
 *       surfaces by the System-designated global querier. <b>See: {@link
 *       SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}</b>
 * </ul>
 *
 * <p>Applications create a database by opening an {@link AppSearchSession}.
 *
 * <p>Example:
 *
 * <pre>
 * AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
 *
 * AppSearchManager.SearchContext searchContext = new AppSearchManager.SearchContext.Builder().
 *    setDatabaseName(dbName).build());
 * appSearchManager.createSearchSession(searchContext, mExecutor, appSearchSessionResult -&gt; {
 *      mAppSearchSession = appSearchSessionResult.getResultValue();
 * });</pre>
 *
 * <p>After opening the session, a schema must be set in order to define the organizational
 * structure of data. The schema is set by calling {@link AppSearchSession#setSchema}. The schema is
 * composed of a collection of {@link AppSearchSchema} objects, each of which defines a unique type
 * of data.
 *
 * <p>Example:
 *
 * <pre>
 * AppSearchSchema emailSchemaType = new AppSearchSchema.Builder("Email")
 *     .addProperty(new StringPropertyConfig.Builder("subject")
 *        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
 *        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
 *        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
 *    .build()
 * ).build();
 *
 * SetSchemaRequest request = new SetSchemaRequest.Builder().addSchema(emailSchemaType).build();
 * mAppSearchSession.set(request, mExecutor, appSearchResult -&gt; {
 *      if (appSearchResult.isSuccess()) {
 *           // Schema has been successfully set.
 *      }
 * });</pre>
 *
 * <p>The basic unit of data in AppSearch is represented as a {@link GenericDocument} object,
 * containing an ID, namespace, time-to-live, score, and properties. A namespace organizes a logical
 * group of documents. For example, a namespace can be created to group documents on a per-account
 * basis. An ID identifies a single document within a namespace. The combination of namespace and ID
 * uniquely identifies a {@link GenericDocument} in the database.
 *
 * <p>Once the schema has been set, {@link GenericDocument} objects can be put into the database and
 * indexed by calling {@link AppSearchSession#put}.
 *
 * <p>Example:
 *
 * <pre>
 * // Although for this example we use GenericDocument directly, we recommend extending
 * // GenericDocument to create specific types (i.e. Email) with specific setters/getters.
 * GenericDocument email = new GenericDocument.Builder<>(NAMESPACE, ID, EMAIL_SCHEMA_TYPE)
 *     .setPropertyString(“subject”, EMAIL_SUBJECT)
 *     .setScore(EMAIL_SCORE)
 *     .build();
 *
 * PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocuments(email)
 *     .build();
 * mAppSearchSession.put(request, mExecutor, appSearchBatchResult -&gt; {
 *      if (appSearchBatchResult.isSuccess()) {
 *           // All documents have been successfully indexed.
 *      }
 * });</pre>
 *
 * <p>Searching within the database is done by calling {@link AppSearchSession#search} and providing
 * the query string to search for, as well as a {@link SearchSpec}.
 *
 * <p>Alternatively, {@link AppSearchSession#getByDocumentId} can be called to retrieve documents by
 * namespace and ID.
 *
 * <p>Document removal is done either by time-to-live expiration, or explicitly calling a remove
 * operation. Remove operations can be done by namespace and ID via {@link
 * AppSearchSession#remove(RemoveByDocumentIdRequest, Executor, BatchResultCallback)}, or by query
 * via {@link AppSearchSession#remove(String, SearchSpec, Executor, Consumer)}.
 */
@SystemService(Context.APP_SEARCH_SERVICE)
public class AppSearchManager {

    private final IAppSearchManager mService;
    private final Context mContext;

    /** @hide */
    public AppSearchManager(@NonNull Context context, @NonNull IAppSearchManager service) {
        mContext = Objects.requireNonNull(context);
        mService = Objects.requireNonNull(service);
    }

    /** Contains information about how to create the search session. */
    public static final class SearchContext {
        final String mDatabaseName;

        SearchContext(@NonNull String databaseName) {
            mDatabaseName = Objects.requireNonNull(databaseName);
        }

        /**
         * Returns the name of the database to create or open.
         *
         * <p>Databases with different names are fully separate with distinct types, namespaces, and
         * data.
         */
        @NonNull
        public String getDatabaseName() {
            return mDatabaseName;
        }

        /** Builder for {@link SearchContext} objects. */
        public static final class Builder {
            private final String mDatabaseName;
            private boolean mBuilt = false;

            /**
             * Creates a new {@link SearchContext.Builder}.
             *
             * <p>{@link AppSearchSession} will create or open a database under the given name.
             *
             * <p>Databases with different names are fully separate with distinct types, namespaces,
             * and data.
             *
             * <p>Database name cannot contain {@code '/'}.
             *
             * @param databaseName The name of the database.
             * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
             */
            public Builder(@NonNull String databaseName) {
                Objects.requireNonNull(databaseName);
                Preconditions.checkArgument(
                        !databaseName.contains("/"), "Database name cannot contain '/'");
                mDatabaseName = databaseName;
            }

            /** Builds a {@link SearchContext} instance. */
            @NonNull
            public SearchContext build() {
                Preconditions.checkState(!mBuilt, "Builder has already been used");
                mBuilt = true;
                return new SearchContext(mDatabaseName);
            }
        }
    }

    /**
     * Creates a new {@link AppSearchSession}.
     *
     * <p>This process requires an AppSearch native indexing file system. If it's not created, the
     * initialization process will create one under the user's credential encrypted directory.
     *
     * @param searchContext The {@link SearchContext} contains all information to create a new
     *     {@link AppSearchSession}
     * @param executor Executor on which to invoke the callback.
     * @param callback The {@link AppSearchResult}&lt;{@link AppSearchSession}&gt; of performing
     *     this operation. Or a {@link AppSearchResult} with failure reason code and error
     *     information.
     */
    @UserHandleAware
    public void createSearchSession(
            @NonNull SearchContext searchContext,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
        Objects.requireNonNull(searchContext);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        AppSearchSession.createSearchSession(
                searchContext,
                mService,
                mContext.getUser(),
                AppSearchAttributionSource.createAttributionSource(
                        mContext, /* callingPid= */ Process.myPid()),
                AppSearchEnvironmentFactory.getEnvironmentInstance().getCacheDir(mContext),
                executor,
                callback);
    }

    /**
     * Creates a new {@link GlobalSearchSession}.
     *
     * <p>This process requires an AppSearch native indexing file system. If it's not created, the
     * initialization process will create one under the user's credential encrypted directory.
     *
     * @param executor Executor on which to invoke the callback.
     * @param callback The {@link AppSearchResult}&lt;{@link GlobalSearchSession}&gt; of performing
     *     this operation. Or a {@link AppSearchResult} with failure reason code and error
     *     information.
     */
    @UserHandleAware
    public void createGlobalSearchSession(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        GlobalSearchSession.createGlobalSearchSession(
                mService,
                mContext.getUser(),
                AppSearchAttributionSource.createAttributionSource(
                        mContext, /* callingPid= */ Process.myPid()),
                executor,
                callback);
    }

    /**
     * Creates a new {@link EnterpriseGlobalSearchSession}
     *
     * <p>EnterpriseGlobalSearchSession queries data from the user’s work profile, allowing apps
     * running on the personal profile to access a limited subset of work profile data. Enterprise
     * access must be explicitly enabled on schemas, and schemas may also specify additional
     * permissions required for enterprise access.
     *
     * <p>This process requires an AppSearch native indexing file system. If it's not created, the
     * initialization process will create one under the user's credential encrypted directory.
     *
     * @param executor Executor on which to invoke the callback.
     * @param callback The {@link AppSearchResult}&lt;{@link EnterpriseGlobalSearchSession}&gt; of
     *     performing this operation. Or a {@link AppSearchResult} with failure reason code and
     *     error information.
     */
    @FlaggedApi(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
    @UserHandleAware
    public void createEnterpriseGlobalSearchSession(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<AppSearchResult<EnterpriseGlobalSearchSession>> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        EnterpriseGlobalSearchSession.createEnterpriseGlobalSearchSession(
                mService,
                mContext.getUser(),
                AppSearchAttributionSource.createAttributionSource(
                        mContext, /* callingPid= */ Process.myPid()),
                executor,
                callback);
    }
}
