1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.app.appsearch;
17 
18 import android.annotation.CallbackExecutor;
19 import android.annotation.FlaggedApi;
20 import android.annotation.NonNull;
21 import android.annotation.SystemService;
22 import android.annotation.UserHandleAware;
23 import android.app.appsearch.aidl.AppSearchAttributionSource;
24 import android.app.appsearch.aidl.IAppSearchManager;
25 import android.app.appsearch.functions.AppFunctionManager;
26 import android.content.Context;
27 import android.os.Process;
28 
29 import com.android.appsearch.flags.Flags;
30 import com.android.internal.util.Preconditions;
31 
32 import java.util.Objects;
33 import java.util.concurrent.Executor;
34 import java.util.function.Consumer;
35 
36 /**
37  * Provides access to the centralized AppSearch index maintained by the system.
38  *
39  * <p>AppSearch is an offline, on-device search library for managing structured data featuring:
40  *
41  * <ul>
42  *   <li>APIs to index and retrieve data via full-text search.
43  *   <li>An API for applications to explicitly grant read-access permission of their data to other
44  *       applications. <b>See: {@link
45  *       SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}</b>
46  *   <li>An API for applications to opt into or out of having their data displayed on System UI
47  *       surfaces by the System-designated global querier. <b>See: {@link
48  *       SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}</b>
49  * </ul>
50  *
51  * <p>Applications create a database by opening an {@link AppSearchSession}.
52  *
53  * <p>Example:
54  *
55  * <pre>
56  * AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
57  *
58  * AppSearchManager.SearchContext searchContext = new AppSearchManager.SearchContext.Builder().
59  *    setDatabaseName(dbName).build());
60  * appSearchManager.createSearchSession(searchContext, mExecutor, appSearchSessionResult -&gt; {
61  *      mAppSearchSession = appSearchSessionResult.getResultValue();
62  * });</pre>
63  *
64  * <p>After opening the session, a schema must be set in order to define the organizational
65  * structure of data. The schema is set by calling {@link AppSearchSession#setSchema}. The schema is
66  * composed of a collection of {@link AppSearchSchema} objects, each of which defines a unique type
67  * of data.
68  *
69  * <p>Example:
70  *
71  * <pre>
72  * AppSearchSchema emailSchemaType = new AppSearchSchema.Builder("Email")
73  *     .addProperty(new StringPropertyConfig.Builder("subject")
74  *        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
75  *        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
76  *        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
77  *    .build()
78  * ).build();
79  *
80  * SetSchemaRequest request = new SetSchemaRequest.Builder().addSchema(emailSchemaType).build();
81  * mAppSearchSession.set(request, mExecutor, appSearchResult -&gt; {
82  *      if (appSearchResult.isSuccess()) {
83  *           // Schema has been successfully set.
84  *      }
85  * });</pre>
86  *
87  * <p>The basic unit of data in AppSearch is represented as a {@link GenericDocument} object,
88  * containing an ID, namespace, time-to-live, score, and properties. A namespace organizes a logical
89  * group of documents. For example, a namespace can be created to group documents on a per-account
90  * basis. An ID identifies a single document within a namespace. The combination of namespace and ID
91  * uniquely identifies a {@link GenericDocument} in the database.
92  *
93  * <p>Once the schema has been set, {@link GenericDocument} objects can be put into the database and
94  * indexed by calling {@link AppSearchSession#put}.
95  *
96  * <p>Example:
97  *
98  * <pre>
99  * // Although for this example we use GenericDocument directly, we recommend extending
100  * // GenericDocument to create specific types (i.e. Email) with specific setters/getters.
101  * GenericDocument email = new GenericDocument.Builder<>(NAMESPACE, ID, EMAIL_SCHEMA_TYPE)
102  *     .setPropertyString(“subject”, EMAIL_SUBJECT)
103  *     .setScore(EMAIL_SCORE)
104  *     .build();
105  *
106  * PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocuments(email)
107  *     .build();
108  * mAppSearchSession.put(request, mExecutor, appSearchBatchResult -&gt; {
109  *      if (appSearchBatchResult.isSuccess()) {
110  *           // All documents have been successfully indexed.
111  *      }
112  * });</pre>
113  *
114  * <p>Searching within the database is done by calling {@link AppSearchSession#search} and providing
115  * the query string to search for, as well as a {@link SearchSpec}.
116  *
117  * <p>Alternatively, {@link AppSearchSession#getByDocumentId} can be called to retrieve documents by
118  * namespace and ID.
119  *
120  * <p>Document removal is done either by time-to-live expiration, or explicitly calling a remove
121  * operation. Remove operations can be done by namespace and ID via {@link
122  * AppSearchSession#remove(RemoveByDocumentIdRequest, Executor, BatchResultCallback)}, or by query
123  * via {@link AppSearchSession#remove(String, SearchSpec, Executor, Consumer)}.
124  */
125 @SystemService(Context.APP_SEARCH_SERVICE)
126 public class AppSearchManager {
127 
128     private final IAppSearchManager mService;
129     private final Context mContext;
130     private final AppFunctionManager mAppFunctionManager;
131 
132     /** @hide */
AppSearchManager(@onNull Context context, @NonNull IAppSearchManager service)133     public AppSearchManager(@NonNull Context context, @NonNull IAppSearchManager service) {
134         mContext = Objects.requireNonNull(context);
135         mService = Objects.requireNonNull(service);
136         mAppFunctionManager = new AppFunctionManager(context, service);
137     }
138 
139     /** Contains information about how to create the search session. */
140     public static final class SearchContext {
141         final String mDatabaseName;
142 
SearchContext(@onNull String databaseName)143         SearchContext(@NonNull String databaseName) {
144             mDatabaseName = Objects.requireNonNull(databaseName);
145         }
146 
147         /**
148          * Returns the name of the database to create or open.
149          *
150          * <p>Databases with different names are fully separate with distinct types, namespaces, and
151          * data.
152          */
153         @NonNull
getDatabaseName()154         public String getDatabaseName() {
155             return mDatabaseName;
156         }
157 
158         /** Builder for {@link SearchContext} objects. */
159         public static final class Builder {
160             private final String mDatabaseName;
161             private boolean mBuilt = false;
162 
163             /**
164              * Creates a new {@link SearchContext.Builder}.
165              *
166              * <p>{@link AppSearchSession} will create or open a database under the given name.
167              *
168              * <p>Databases with different names are fully separate with distinct types, namespaces,
169              * and data.
170              *
171              * <p>Database name cannot contain {@code '/'}.
172              *
173              * @param databaseName The name of the database.
174              * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
175              */
Builder(@onNull String databaseName)176             public Builder(@NonNull String databaseName) {
177                 Objects.requireNonNull(databaseName);
178                 Preconditions.checkArgument(
179                         !databaseName.contains("/"), "Database name cannot contain '/'");
180                 mDatabaseName = databaseName;
181             }
182 
183             /** Builds a {@link SearchContext} instance. */
184             @NonNull
build()185             public SearchContext build() {
186                 Preconditions.checkState(!mBuilt, "Builder has already been used");
187                 mBuilt = true;
188                 return new SearchContext(mDatabaseName);
189             }
190         }
191     }
192 
193     /**
194      * Creates a new {@link AppSearchSession}.
195      *
196      * <p>This process requires an AppSearch native indexing file system. If it's not created, the
197      * initialization process will create one under the user's credential encrypted directory.
198      *
199      * @param searchContext The {@link SearchContext} contains all information to create a new
200      *     {@link AppSearchSession}
201      * @param executor Executor on which to invoke the callback.
202      * @param callback The {@link AppSearchResult}&lt;{@link AppSearchSession}&gt; of performing
203      *     this operation. Or a {@link AppSearchResult} with failure reason code and error
204      *     information.
205      */
206     @UserHandleAware
createSearchSession( @onNull SearchContext searchContext, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<AppSearchSession>> callback)207     public void createSearchSession(
208             @NonNull SearchContext searchContext,
209             @NonNull @CallbackExecutor Executor executor,
210             @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
211         Objects.requireNonNull(searchContext);
212         Objects.requireNonNull(executor);
213         Objects.requireNonNull(callback);
214         AppSearchSession.createSearchSession(
215                 searchContext,
216                 mService,
217                 mContext.getUser(),
218                 AppSearchAttributionSource.createAttributionSource(
219                         mContext, /* callingPid= */ Process.myPid()),
220                 AppSearchEnvironmentFactory.getEnvironmentInstance().getCacheDir(mContext),
221                 executor,
222                 callback);
223     }
224 
225     /**
226      * Creates a new {@link GlobalSearchSession}.
227      *
228      * <p>This process requires an AppSearch native indexing file system. If it's not created, the
229      * initialization process will create one under the user's credential encrypted directory.
230      *
231      * @param executor Executor on which to invoke the callback.
232      * @param callback The {@link AppSearchResult}&lt;{@link GlobalSearchSession}&gt; of performing
233      *     this operation. Or a {@link AppSearchResult} with failure reason code and error
234      *     information.
235      */
236     @UserHandleAware
createGlobalSearchSession( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback)237     public void createGlobalSearchSession(
238             @NonNull @CallbackExecutor Executor executor,
239             @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
240         Objects.requireNonNull(executor);
241         Objects.requireNonNull(callback);
242         GlobalSearchSession.createGlobalSearchSession(
243                 mService,
244                 mContext.getUser(),
245                 AppSearchAttributionSource.createAttributionSource(
246                         mContext, /* callingPid= */ Process.myPid()),
247                 executor,
248                 callback);
249     }
250 
251     /**
252      * Creates a new {@link EnterpriseGlobalSearchSession}
253      *
254      * <p>EnterpriseGlobalSearchSession queries data from the user’s work profile, allowing apps
255      * running on the personal profile to access a limited subset of work profile data. Enterprise
256      * access must be explicitly enabled on schemas, and schemas may also specify additional
257      * permissions required for enterprise access.
258      *
259      * <p>This process requires an AppSearch native indexing file system. If it's not created, the
260      * initialization process will create one under the user's credential encrypted directory.
261      *
262      * @param executor Executor on which to invoke the callback.
263      * @param callback The {@link AppSearchResult}&lt;{@link EnterpriseGlobalSearchSession}&gt; of
264      *     performing this operation. Or a {@link AppSearchResult} with failure reason code and
265      *     error information.
266      */
267     @FlaggedApi(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
268     @UserHandleAware
createEnterpriseGlobalSearchSession( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<EnterpriseGlobalSearchSession>> callback)269     public void createEnterpriseGlobalSearchSession(
270             @NonNull @CallbackExecutor Executor executor,
271             @NonNull Consumer<AppSearchResult<EnterpriseGlobalSearchSession>> callback) {
272         Objects.requireNonNull(executor);
273         Objects.requireNonNull(callback);
274         EnterpriseGlobalSearchSession.createEnterpriseGlobalSearchSession(
275                 mService,
276                 mContext.getUser(),
277                 AppSearchAttributionSource.createAttributionSource(
278                         mContext, /* callingPid= */ Process.myPid()),
279                 executor,
280                 callback);
281     }
282 
283     /** Returns an instance of {@link android.app.appsearch.functions.AppFunctionManager}. */
284     @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
285     @NonNull
getAppFunctionManager()286     public AppFunctionManager getAppFunctionManager() {
287         return mAppFunctionManager;
288     }
289 }
290