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 package androidx.appsearch.platformstorage; 17 18 import android.app.appsearch.AppSearchResult; 19 import android.app.appsearch.BatchResultCallback; 20 import android.content.Context; 21 import android.os.Build; 22 23 import androidx.annotation.DoNotInline; 24 import androidx.annotation.GuardedBy; 25 import androidx.annotation.RequiresApi; 26 import androidx.annotation.RestrictTo; 27 import androidx.appsearch.app.AppSearchBatchResult; 28 import androidx.appsearch.app.Features; 29 import androidx.appsearch.app.GenericDocument; 30 import androidx.appsearch.app.GetByDocumentIdRequest; 31 import androidx.appsearch.app.GetSchemaResponse; 32 import androidx.appsearch.app.GlobalSearchSession; 33 import androidx.appsearch.app.ReportSystemUsageRequest; 34 import androidx.appsearch.app.SearchResults; 35 import androidx.appsearch.app.SearchSpec; 36 import androidx.appsearch.exceptions.AppSearchException; 37 import androidx.appsearch.observer.DocumentChangeInfo; 38 import androidx.appsearch.observer.ObserverCallback; 39 import androidx.appsearch.observer.ObserverSpec; 40 import androidx.appsearch.observer.SchemaChangeInfo; 41 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter; 42 import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter; 43 import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter; 44 import androidx.appsearch.platformstorage.converter.ObserverSpecToPlatformConverter; 45 import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter; 46 import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter; 47 import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter; 48 import androidx.collection.ArrayMap; 49 import androidx.concurrent.futures.ResolvableFuture; 50 import androidx.core.util.Preconditions; 51 52 import com.google.common.util.concurrent.ListenableFuture; 53 54 import org.jspecify.annotations.NonNull; 55 56 import java.util.Map; 57 import java.util.concurrent.Executor; 58 import java.util.function.Consumer; 59 60 /** 61 * An implementation of {@link GlobalSearchSession} which proxies to a 62 * platform {@link android.app.appsearch.GlobalSearchSession}. 63 * 64 * @exportToFramework:hide 65 */ 66 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 67 @RequiresApi(Build.VERSION_CODES.S) 68 class GlobalSearchSessionImpl implements GlobalSearchSession { 69 private final android.app.appsearch.GlobalSearchSession mPlatformSession; 70 private final Executor mExecutor; 71 private final Context mContext; 72 private final Features mFeatures; 73 74 // Management of observer callbacks. 75 @GuardedBy("mObserverCallbacksLocked") 76 private final Map<ObserverCallback, android.app.appsearch.observer.ObserverCallback> 77 mObserverCallbacksLocked = new ArrayMap<>(); 78 GlobalSearchSessionImpl( android.app.appsearch.@onNull GlobalSearchSession platformSession, @NonNull Executor executor, @NonNull Context context)79 GlobalSearchSessionImpl( 80 android.app.appsearch.@NonNull GlobalSearchSession platformSession, 81 @NonNull Executor executor, 82 @NonNull Context context) { 83 mPlatformSession = Preconditions.checkNotNull(platformSession); 84 mExecutor = Preconditions.checkNotNull(executor); 85 mContext = Preconditions.checkNotNull(context); 86 mFeatures = new FeaturesImpl(mContext); 87 } 88 89 @Override 90 public @NonNull ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync( @onNull String packageName, @NonNull String databaseName, @NonNull GetByDocumentIdRequest request)91 getByDocumentIdAsync( 92 @NonNull String packageName, @NonNull String databaseName, 93 @NonNull GetByDocumentIdRequest request) { 94 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 95 throw new UnsupportedOperationException(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID 96 + " is not supported on this AppSearch implementation."); 97 } 98 Preconditions.checkNotNull(packageName); 99 Preconditions.checkNotNull(databaseName); 100 Preconditions.checkNotNull(request); 101 ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future = 102 ResolvableFuture.create(); 103 ApiHelperForT.getByDocumentId(mPlatformSession, packageName, databaseName, 104 RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request), mExecutor, 105 new BatchResultCallbackAdapter<>( 106 future, GenericDocumentToPlatformConverter::toJetpackGenericDocument)); 107 return future; 108 } 109 110 @Override search( @onNull String queryExpression, @NonNull SearchSpec searchSpec)111 public @NonNull SearchResults search( 112 @NonNull String queryExpression, 113 @NonNull SearchSpec searchSpec) { 114 Preconditions.checkNotNull(queryExpression); 115 Preconditions.checkNotNull(searchSpec); 116 android.app.appsearch.SearchResults platformSearchResults = 117 mPlatformSession.search( 118 queryExpression, 119 SearchSpecToPlatformConverter.toPlatformSearchSpec(mContext, searchSpec)); 120 return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor, mContext); 121 } 122 123 @Override reportSystemUsageAsync( @onNull ReportSystemUsageRequest request)124 public @NonNull ListenableFuture<Void> reportSystemUsageAsync( 125 @NonNull ReportSystemUsageRequest request) { 126 Preconditions.checkNotNull(request); 127 ResolvableFuture<Void> future = ResolvableFuture.create(); 128 mPlatformSession.reportSystemUsage( 129 RequestToPlatformConverter.toPlatformReportSystemUsageRequest(request), 130 mExecutor, 131 result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture( 132 result, future)); 133 return future; 134 } 135 136 @Override getSchemaAsync(@onNull String packageName, @NonNull String databaseName)137 public @NonNull ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName, 138 @NonNull String databaseName) { 139 // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an 140 // unsupported build. 141 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 142 throw new UnsupportedOperationException( 143 Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA 144 + " is not supported on this AppSearch implementation."); 145 } 146 ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create(); 147 ApiHelperForT.getSchema(mPlatformSession, packageName, databaseName, mExecutor, 148 result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture( 149 result, 150 future, 151 GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse)); 152 return future; 153 } 154 155 @Override getFeatures()156 public @NonNull Features getFeatures() { 157 return mFeatures; 158 } 159 160 @Override registerObserverCallback( @onNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observer)161 public void registerObserverCallback( 162 @NonNull String targetPackageName, 163 @NonNull ObserverSpec spec, 164 @NonNull Executor executor, 165 @NonNull ObserverCallback observer) throws AppSearchException { 166 Preconditions.checkNotNull(targetPackageName); 167 Preconditions.checkNotNull(spec); 168 Preconditions.checkNotNull(executor); 169 Preconditions.checkNotNull(observer); 170 // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an 171 // unsupported build. 172 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 173 throw new UnsupportedOperationException( 174 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK 175 + " is not supported on this AppSearch implementation"); 176 } 177 178 synchronized (mObserverCallbacksLocked) { 179 android.app.appsearch.observer.ObserverCallback frameworkCallback = 180 mObserverCallbacksLocked.get(observer); 181 if (frameworkCallback == null) { 182 // No stub is associated with this package and observer, so we must create one. 183 frameworkCallback = new android.app.appsearch.observer.ObserverCallback() { 184 @Override 185 public void onSchemaChanged( 186 android.app.appsearch.observer.@NonNull SchemaChangeInfo 187 platformSchemaChangeInfo) { 188 SchemaChangeInfo jetpackSchemaChangeInfo = 189 ObserverSpecToPlatformConverter.toJetpackSchemaChangeInfo( 190 platformSchemaChangeInfo); 191 observer.onSchemaChanged(jetpackSchemaChangeInfo); 192 } 193 194 @Override 195 public void onDocumentChanged( 196 android.app.appsearch.observer.@NonNull DocumentChangeInfo 197 platformDocumentChangeInfo) { 198 DocumentChangeInfo jetpackDocumentChangeInfo = 199 ObserverSpecToPlatformConverter.toJetpackDocumentChangeInfo( 200 platformDocumentChangeInfo); 201 observer.onDocumentChanged(jetpackDocumentChangeInfo); 202 } 203 }; 204 } 205 206 // Regardless of whether this stub was fresh or not, we have to register it again 207 // because the user might be supplying a different spec. 208 try { 209 ApiHelperForT.registerObserverCallback(mPlatformSession, targetPackageName, 210 ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec), executor, 211 frameworkCallback); 212 } catch (android.app.appsearch.exceptions.AppSearchException e) { 213 throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause()); 214 } 215 216 // Now that registration has succeeded, save this stub into our in-memory cache. This 217 // isn't done when errors occur because the user may not call removeObserver if 218 // addObserver threw. 219 mObserverCallbacksLocked.put(observer, frameworkCallback); 220 } 221 } 222 223 @Override unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)224 public void unregisterObserverCallback( 225 @NonNull String targetPackageName, @NonNull ObserverCallback observer) 226 throws AppSearchException { 227 Preconditions.checkNotNull(targetPackageName); 228 Preconditions.checkNotNull(observer); 229 // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an 230 // unsupported build. 231 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 232 throw new UnsupportedOperationException( 233 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK 234 + " is not supported on this AppSearch implementation"); 235 } 236 237 android.app.appsearch.observer.ObserverCallback frameworkCallback; 238 synchronized (mObserverCallbacksLocked) { 239 frameworkCallback = mObserverCallbacksLocked.get(observer); 240 if (frameworkCallback == null) { 241 return; // No such observer registered. Nothing to do. 242 } 243 244 try { 245 ApiHelperForT.unregisterObserverCallback(mPlatformSession, targetPackageName, 246 frameworkCallback); 247 } catch (android.app.appsearch.exceptions.AppSearchException e) { 248 throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause()); 249 } 250 251 // Only remove from the in-memory map once removal from the service side succeeds 252 mObserverCallbacksLocked.remove(observer); 253 } 254 } 255 256 @Override close()257 public void close() { 258 mPlatformSession.close(); 259 } 260 261 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 262 private static class ApiHelperForT { ApiHelperForT()263 private ApiHelperForT() { 264 // This class is not instantiable. 265 } 266 267 @DoNotInline getByDocumentId(android.app.appsearch.GlobalSearchSession platformSession, String packageName, String databaseName, android.app.appsearch.GetByDocumentIdRequest request, Executor executor, BatchResultCallback<String, android.app.appsearch.GenericDocument> callback)268 static void getByDocumentId(android.app.appsearch.GlobalSearchSession platformSession, 269 String packageName, String databaseName, 270 android.app.appsearch.GetByDocumentIdRequest request, Executor executor, 271 BatchResultCallback<String, android.app.appsearch.GenericDocument> callback) { 272 platformSession.getByDocumentId(packageName, databaseName, request, executor, callback); 273 } 274 275 @DoNotInline getSchema(android.app.appsearch.GlobalSearchSession platformSessions, String packageName, String databaseName, Executor executor, Consumer<AppSearchResult<android.app.appsearch.GetSchemaResponse>> callback)276 static void getSchema(android.app.appsearch.GlobalSearchSession platformSessions, 277 String packageName, String databaseName, Executor executor, 278 Consumer<AppSearchResult<android.app.appsearch.GetSchemaResponse>> callback) { 279 platformSessions.getSchema(packageName, databaseName, executor, callback); 280 } 281 282 @DoNotInline registerObserverCallback( android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, android.app.appsearch.observer.ObserverSpec spec, Executor executor, android.app.appsearch.observer.ObserverCallback observer)283 static void registerObserverCallback( 284 android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, 285 android.app.appsearch.observer.ObserverSpec spec, Executor executor, 286 android.app.appsearch.observer.ObserverCallback observer) 287 throws android.app.appsearch.exceptions.AppSearchException { 288 platformSession.registerObserverCallback(targetPackageName, spec, executor, observer); 289 } 290 291 @DoNotInline unregisterObserverCallback( android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, android.app.appsearch.observer.ObserverCallback observer)292 static void unregisterObserverCallback( 293 android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, 294 android.app.appsearch.observer.ObserverCallback observer) 295 throws android.app.appsearch.exceptions.AppSearchException { 296 platformSession.unregisterObserverCallback(targetPackageName, observer); 297 } 298 } 299 } 300