blob: 758e63bc457a4f35afc7c87d3bf3531254305f50 [file] [log] [blame]
/*
* Copyright 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.
*/
// @exportToFramework:skipFile()
package androidx.appsearch.localstorage;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.stats.InitializeStats;
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.util.FutureUtil;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* An AppSearch storage system which stores data locally in the app's storage space using a bundled
* version of the search native library.
*
* <p>The search native library is an on-device searching library that allows apps to define
* {@link androidx.appsearch.app.AppSearchSchema}s, save and query a variety of
* {@link Document}s. The library needs to be initialized
* before using, which will create a folder to save data in the app's storage space.
*
* <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
* delete, etc..).
*/
public class LocalStorage {
private static final String TAG = "AppSearchLocalStorage";
private static final String ICING_LIB_ROOT_DIR = "appsearch";
/** Contains information about how to create the search session. */
public static final class SearchContext {
final Context mContext;
final String mDatabaseName;
final Executor mExecutor;
@Nullable
final AppSearchLogger mLogger;
SearchContext(@NonNull Context context, @NonNull String databaseName,
@NonNull Executor executor, @Nullable AppSearchLogger logger) {
mContext = Preconditions.checkNotNull(context);
mDatabaseName = Preconditions.checkNotNull(databaseName);
mExecutor = Preconditions.checkNotNull(executor);
mLogger = logger;
}
/**
* Returns the name of the database to create or open.
*/
@NonNull
public String getDatabaseName() {
return mDatabaseName;
}
/**
* Returns the worker executor associated with {@link AppSearchSession}.
*
* <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
* be returned. You should never cast the executor to
* {@link java.util.concurrent.ExecutorService} and call
* {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
* since {@link Executor#execute} won't return anything, we will hang forever waiting for
* the execution.
*/
@NonNull
public Executor getWorkerExecutor() {
return mExecutor;
}
/** Builder for {@link SearchContext} objects. */
public static final class Builder {
private final Context mContext;
private final String mDatabaseName;
private Executor mExecutor;
@Nullable
private AppSearchLogger mLogger;
/**
* Creates a {@link SearchContext.Builder} instance.
*
* <p>{@link AppSearchSession} will create or open a database under the given name.
*
* <p>Databases with different names are fully separate with distinct schema types,
* namespaces, and documents.
*
* <p>The database name cannot contain {@code '/'}.
*
* @param databaseName The name of the database.
* @throws IllegalArgumentException if the databaseName contains {@code '/'}.
*/
public Builder(@NonNull Context context, @NonNull String databaseName) {
mContext = Preconditions.checkNotNull(context);
Preconditions.checkNotNull(databaseName);
if (databaseName.contains("/")) {
throw new IllegalArgumentException("Database name cannot contain '/'");
}
mDatabaseName = databaseName;
}
/**
* Sets the worker executor associated with {@link AppSearchSession}.
*
* <p>If an executor is not provided, the AppSearch default executor will be used.
*
* @param executor the worker executor used to run heavy background tasks.
*/
@NonNull
public Builder setWorkerExecutor(@NonNull Executor executor) {
mExecutor = Preconditions.checkNotNull(executor);
return this;
}
/**
* Sets the custom logger used to get the details stats from AppSearch.
*
* <p>If no logger is provided, nothing would be returned/logged. There is no default
* logger implementation in AppSearch.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Builder setLogger(@NonNull AppSearchLogger logger) {
mLogger = Preconditions.checkNotNull(logger);
return this;
}
/** Builds a {@link SearchContext} instance. */
@NonNull
public SearchContext build() {
if (mExecutor == null) {
mExecutor = EXECUTOR;
}
return new SearchContext(mContext, mDatabaseName, mExecutor, mLogger);
}
}
}
/** Contains information relevant to creating a global search session. */
public static final class GlobalSearchContext {
final Context mContext;
final Executor mExecutor;
@Nullable
final AppSearchLogger mLogger;
GlobalSearchContext(@NonNull Context context, @NonNull Executor executor,
@Nullable AppSearchLogger logger) {
mContext = Preconditions.checkNotNull(context);
mExecutor = Preconditions.checkNotNull(executor);
mLogger = logger;
}
/**
* Returns the worker executor associated with {@link GlobalSearchSession}.
*
* <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
* be returned. You should never cast the executor to
* {@link java.util.concurrent.ExecutorService} and call
* {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
* since {@link Executor#execute} won't return anything, we will hang forever waiting for
* the execution.
*/
@NonNull
public Executor getWorkerExecutor() {
return mExecutor;
}
/** Builder for {@link GlobalSearchContext} objects. */
public static final class Builder {
private final Context mContext;
private Executor mExecutor;
@Nullable
private AppSearchLogger mLogger;
public Builder(@NonNull Context context) {
mContext = Preconditions.checkNotNull(context);
}
/**
* Sets the worker executor associated with {@link GlobalSearchSession}.
*
* <p>If an executor is not provided, the AppSearch default executor will be used.
*
* @param executor the worker executor used to run heavy background tasks.
*/
@NonNull
public Builder setWorkerExecutor(@NonNull Executor executor) {
Preconditions.checkNotNull(executor);
mExecutor = executor;
return this;
}
/**
* Sets the custom logger used to get the details stats from AppSearch.
*
* <p>If no logger is provided, nothing would be returned/logged. There is no default
* logger implementation in AppSearch.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Builder setLogger(@NonNull AppSearchLogger logger) {
mLogger = Preconditions.checkNotNull(logger);
return this;
}
/** Builds a {@link GlobalSearchContext} instance. */
@NonNull
public GlobalSearchContext build() {
if (mExecutor == null) {
mExecutor = EXECUTOR;
}
return new GlobalSearchContext(mContext, mExecutor, mLogger);
}
}
}
// AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
// mutate requests will need to gain write lock and query requests need to gain read lock.
static final Executor EXECUTOR = Executors.newCachedThreadPool();
private static volatile LocalStorage sInstance;
private final AppSearchImpl mAppSearchImpl;
/**
* Opens a new {@link AppSearchSession} on this storage with executor.
*
* <p>This process requires a native search library. If it's not created, the initialization
* process will create one.
*
* @param context The {@link SearchContext} contains all information to create a new
* {@link AppSearchSession}
*/
@NonNull
public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull SearchContext context) {
Preconditions.checkNotNull(context);
return FutureUtil.execute(context.mExecutor, () -> {
LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
context.mLogger);
return instance.doCreateSearchSession(context);
});
}
/**
* Opens a new {@link GlobalSearchSession} on this storage.
*
* <p>The {@link GlobalSearchSession} opened from this {@link LocalStorage} allows the user to
* search across all local databases within the {@link LocalStorage} of this app, however
* cross-app search is not possible with {@link LocalStorage}.
*
* <p>This process requires a native search library. If it's not created, the initialization
* process will create one.
*/
@NonNull
public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
@NonNull GlobalSearchContext context) {
Preconditions.checkNotNull(context);
return FutureUtil.execute(context.mExecutor, () -> {
LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
context.mLogger);
return instance.doCreateGlobalSearchSession(context);
});
}
/**
* Returns the singleton instance of {@link LocalStorage}.
*
* <p>If the system is not initialized, it will be initialized using the provided
* {@code context}.
*/
@NonNull
@WorkerThread
@VisibleForTesting
static LocalStorage getOrCreateInstance(@NonNull Context context, @NonNull Executor executor,
@Nullable AppSearchLogger logger)
throws AppSearchException {
Preconditions.checkNotNull(context);
if (sInstance == null) {
synchronized (LocalStorage.class) {
if (sInstance == null) {
sInstance = new LocalStorage(context, executor, logger);
}
}
}
return sInstance;
}
@WorkerThread
private LocalStorage(
@NonNull Context context,
@NonNull Executor executor,
@Nullable AppSearchLogger logger)
throws AppSearchException {
Preconditions.checkNotNull(context);
File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
InitializeStats.Builder initStatsBuilder = null;
if (logger != null) {
initStatsBuilder = new InitializeStats.Builder();
}
// Syncing the current logging level to Icing before creating the AppSearch object, so that
// the correct logging level will cover the period of Icing initialization.
AppSearchImpl.syncLoggingLevelToIcing();
mAppSearchImpl = AppSearchImpl.create(
icingDir,
new UnlimitedLimitConfig(),
new DefaultIcingOptionsConfig(),
initStatsBuilder,
new JetpackOptimizeStrategy(),
/*visibilityChecker=*/null);
if (logger != null) {
initStatsBuilder.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
logger.logStats(initStatsBuilder.build());
}
executor.execute(() -> {
long totalOptimizeLatencyStartMillis = SystemClock.elapsedRealtime();
OptimizeStats.Builder builder = null;
try {
if (logger != null) {
builder = new OptimizeStats.Builder();
}
mAppSearchImpl.checkForOptimize(builder);
} catch (AppSearchException e) {
Log.w(TAG, "Error occurred when check for optimize", e);
} finally {
if (builder != null) {
OptimizeStats oStats = builder
.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime()
- totalOptimizeLatencyStartMillis))
.build();
if (logger != null && oStats.getOriginalDocumentCount() > 0) {
// see if optimize has been run by checking originalDocumentCount
logger.logStats(builder.build());
}
}
}
});
}
@NonNull
private AppSearchSession doCreateSearchSession(@NonNull SearchContext context) {
return new SearchSessionImpl(
mAppSearchImpl,
context.mExecutor,
new AlwaysSupportedFeatures(),
context.mContext,
context.mDatabaseName,
context.mLogger);
}
@NonNull
private GlobalSearchSession doCreateGlobalSearchSession(
@NonNull GlobalSearchContext context) {
return new GlobalSearchSessionImpl(mAppSearchImpl, context.mExecutor,
new AlwaysSupportedFeatures(), context.mContext, context.mLogger);
}
}