/* * 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.app.appsearch.functions.AppFunctionManager; 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. * *

AppSearch is an offline, on-device search library for managing structured data featuring: * *

* *

Applications create a database by opening an {@link AppSearchSession}. * *

Example: * *

 * AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
 *
 * AppSearchManager.SearchContext searchContext = new AppSearchManager.SearchContext.Builder().
 *    setDatabaseName(dbName).build());
 * appSearchManager.createSearchSession(searchContext, mExecutor, appSearchSessionResult -> {
 *      mAppSearchSession = appSearchSessionResult.getResultValue();
 * });
* *

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. * *

Example: * *

 * 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 -> {
 *      if (appSearchResult.isSuccess()) {
 *           // Schema has been successfully set.
 *      }
 * });
* *

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. * *

Once the schema has been set, {@link GenericDocument} objects can be put into the database and * indexed by calling {@link AppSearchSession#put}. * *

Example: * *

 * // 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 -> {
 *      if (appSearchBatchResult.isSuccess()) {
 *           // All documents have been successfully indexed.
 *      }
 * });
* *

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}. * *

Alternatively, {@link AppSearchSession#getByDocumentId} can be called to retrieve documents by * namespace and ID. * *

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; private final AppFunctionManager mAppFunctionManager; /** @hide */ public AppSearchManager(@NonNull Context context, @NonNull IAppSearchManager service) { mContext = Objects.requireNonNull(context); mService = Objects.requireNonNull(service); mAppFunctionManager = new AppFunctionManager(context, 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. * *

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}. * *

{@link AppSearchSession} will create or open a database under the given name. * *

Databases with different names are fully separate with distinct types, namespaces, * and data. * *

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}. * *

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}<{@link AppSearchSession}> 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> 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}. * *

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}<{@link GlobalSearchSession}> 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> 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} * *

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. * *

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}<{@link EnterpriseGlobalSearchSession}> 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> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); EnterpriseGlobalSearchSession.createEnterpriseGlobalSearchSession( mService, mContext.getUser(), AppSearchAttributionSource.createAttributionSource( mContext, /* callingPid= */ Process.myPid()), executor, callback); } /** Returns an instance of {@link android.app.appsearch.functions.AppFunctionManager}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @NonNull public AppFunctionManager getAppFunctionManager() { return mAppFunctionManager; } }