1 /*
2  * Copyright 2021 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 
17 package androidx.appsearch.debugview.samples;
18 
19 import android.content.Context;
20 
21 import androidx.appsearch.app.AppSearchBatchResult;
22 import androidx.appsearch.app.AppSearchSession;
23 import androidx.appsearch.app.PutDocumentsRequest;
24 import androidx.appsearch.app.SetSchemaRequest;
25 import androidx.appsearch.app.SetSchemaResponse;
26 import androidx.appsearch.debugview.samples.model.Note;
27 import androidx.appsearch.exceptions.AppSearchException;
28 import androidx.appsearch.localstorage.LocalStorage;
29 
30 import com.google.common.util.concurrent.Futures;
31 import com.google.common.util.concurrent.ListenableFuture;
32 import com.google.common.util.concurrent.SettableFuture;
33 
34 import org.jspecify.annotations.NonNull;
35 
36 import java.io.Closeable;
37 import java.util.List;
38 import java.util.concurrent.Executor;
39 
40 /**
41  * Manages interactions with AppSearch.
42  */
43 public class NotesAppSearchManager implements Closeable {
44     private static final String DB_NAME = "notesDb";
45     private static final boolean FORCE_OVERRIDE = true;
46 
47     private final Context mContext;
48     private final Executor mExecutor;
49     private final SettableFuture<AppSearchSession> mAppSearchSessionFuture =
50             SettableFuture.create();
51 
NotesAppSearchManager(@onNull Context context, @NonNull Executor executor)52     private NotesAppSearchManager(@NonNull Context context, @NonNull Executor executor) {
53         mContext = context;
54         mExecutor = executor;
55     }
56 
57     /**
58      * Factory for creating a {@link NotesAppSearchManager} instance.
59      *
60      * <p>This creates and initializes an {@link AppSearchSession}. It also resets existing
61      * {@link Note} objects from the index and re-adds the {@link Note} document class to the
62      * AppSearch schema.
63      *
64      * @param executor to run AppSearch operations on.
65      */
createNotesAppSearchManager( @onNull Context context, @NonNull Executor executor)66     public static @NonNull ListenableFuture<NotesAppSearchManager> createNotesAppSearchManager(
67             @NonNull Context context, @NonNull Executor executor) {
68         NotesAppSearchManager notesAppSearchManager = new NotesAppSearchManager(context, executor);
69         return Futures.transform(notesAppSearchManager.initialize(),
70                 unused -> notesAppSearchManager, executor);
71     }
72 
73     /**
74      * Closes the AppSearch session.
75      */
76     @Override
close()77     public void close() {
78         Futures.whenAllSucceed(mAppSearchSessionFuture).call(() -> {
79             Futures.getDone(mAppSearchSessionFuture).close();
80             return null;
81         }, mExecutor);
82     }
83 
84     /**
85      * Inserts {@link Note} documents into the AppSearch database.
86      *
87      * @param notes list of notes to index in AppSearch.
88      */
insertNotes( @onNull List<Note> notes)89     public @NonNull ListenableFuture<AppSearchBatchResult<String, Void>> insertNotes(
90             @NonNull List<Note> notes) {
91         try {
92             PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(notes)
93                     .build();
94             return Futures.transformAsync(mAppSearchSessionFuture,
95                     session -> session.putAsync(request), mExecutor);
96         } catch (Exception e) {
97             return Futures.immediateFailedFuture(e);
98         }
99     }
100 
initialize()101     private @NonNull ListenableFuture<Void> initialize() {
102         return Futures.transformAsync(createLocalSession(), session -> {
103             mAppSearchSessionFuture.set(session);
104             return Futures.transformAsync(resetDocuments(),
105                     unusedResetResult -> Futures.transform(setSchema(),
106                             unusedSetSchemaResult -> null,
107                             mExecutor),
108                     mExecutor);
109         }, mExecutor);
110     }
111 
createLocalSession()112     private ListenableFuture<AppSearchSession> createLocalSession() {
113         return LocalStorage.createSearchSessionAsync(
114                 new LocalStorage.SearchContext.Builder(mContext, DB_NAME)
115                         .build()
116         );
117     }
118 
resetDocuments()119     private ListenableFuture<SetSchemaResponse> resetDocuments() {
120         SetSchemaRequest request =
121                 new SetSchemaRequest.Builder().setForceOverride(FORCE_OVERRIDE).build();
122         return Futures.transformAsync(mAppSearchSessionFuture,
123                 session -> session.setSchemaAsync(request),
124                 mExecutor);
125     }
126 
setSchema()127     private ListenableFuture<SetSchemaResponse> setSchema() {
128         try {
129             SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
130                     .build();
131             return Futures.transformAsync(mAppSearchSessionFuture,
132                     session -> session.setSchemaAsync(request), mExecutor);
133         } catch (AppSearchException e) {
134             return Futures.immediateFailedFuture(e);
135         }
136     }
137 }
138