diff options
-rw-r--r-- | Android.bp | 8 | ||||
-rw-r--r-- | TEST_MAPPING | 5 | ||||
-rw-r--r-- | icing/icing-search-engine_jni.cc | 285 | ||||
-rw-r--r-- | icing/jni.lds | 10 | ||||
-rw-r--r-- | java/Android.bp | 22 | ||||
-rw-r--r-- | java/src/com/google/android/icing/IcingSearchEngine.java | 347 | ||||
-rw-r--r-- | java/tests/instrumentation/Android.bp | 42 | ||||
-rw-r--r-- | java/tests/instrumentation/AndroidManifest.xml | 30 | ||||
-rw-r--r-- | java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java | 284 |
9 files changed, 1030 insertions, 3 deletions
@@ -54,7 +54,7 @@ cc_defaults { } cc_library_static { - name: "libicing_proto_lite", + name: "icing-c-proto", defaults: ["libicing_defaults"], proto: { type: "lite", @@ -67,7 +67,7 @@ cc_library_static { } cc_library_shared { - name: "libicing", + name: "libicing_jni", defaults: ["libicing_defaults"], srcs: [ "icing/**/*.cc", @@ -83,7 +83,7 @@ cc_library_shared { "icing/tools/**/*", ], static_libs: [ - "libicing_proto_lite", + "icing-c-proto", "libutf", ], shared_libs: [ @@ -94,6 +94,8 @@ cc_library_shared { "libprotobuf-cpp-full", "libz", ], + + version_script: "icing/jni.lds", } // TODO(cassiewang): Add build rules and a TEST_MAPPING for cc_tests diff --git a/TEST_MAPPING b/TEST_MAPPING index 12188f8..37cb5fc 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,4 +1,9 @@ { + "presubmit": [ + { + "name": "IcingSearchEngineTest" + } + ], "imports": [ { "path": "frameworks/base/apex/appsearch/service/java/com/android/server/appsearch" diff --git a/icing/icing-search-engine_jni.cc b/icing/icing-search-engine_jni.cc new file mode 100644 index 0000000..263c4f9 --- /dev/null +++ b/icing/icing-search-engine_jni.cc @@ -0,0 +1,285 @@ +// 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. + +#include <google/protobuf/message_lite.h> +#include <jni.h> + +#include <string> + +#include "icing/absl_ports/status_imports.h" +#include "icing/icing-search-engine.h" +#include "icing/proto/document.pb.h" +#include "icing/proto/initialize.pb.h" +#include "icing/proto/optimize.pb.h" +#include "icing/proto/persist.pb.h" +#include "icing/proto/schema.pb.h" +#include "icing/proto/search.pb.h" +#include "icing/util/logging.h" + +namespace { + +bool ParseProtoFromJniByteArray(JNIEnv* env, jbyteArray bytes, + google::protobuf::MessageLite* protobuf) { + int bytes_size = env->GetArrayLength(bytes); + uint8_t* bytes_ptr = static_cast<uint8_t*>( + env->GetPrimitiveArrayCritical(bytes, /*isCopy=*/nullptr)); + bool parsed = protobuf->ParseFromArray(bytes_ptr, bytes_size); + env->ReleasePrimitiveArrayCritical(bytes, bytes_ptr, /*mode=*/0); + + return parsed; +} + +jbyteArray SerializeProtoToJniByteArray( + JNIEnv* env, const google::protobuf::MessageLite& protobuf) { + int size = protobuf.ByteSizeLong(); + jbyteArray ret = env->NewByteArray(size); + if (ret == nullptr) { + ICING_LOG(ERROR) << "Failed to allocated bytes for jni protobuf"; + return nullptr; + } + + uint8_t* ret_buf = static_cast<uint8_t*>( + env->GetPrimitiveArrayCritical(ret, /*isCopy=*/nullptr)); + protobuf.SerializeWithCachedSizesToArray(ret_buf); + env->ReleasePrimitiveArrayCritical(ret, ret_buf, 0); + return ret; +} + +icing::lib::IcingSearchEngine* GetIcingSearchEnginePointer( + jlong native_pointer) { + return reinterpret_cast<icing::lib::IcingSearchEngine*>(native_pointer); +} + +} // namespace + +extern "C" { + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + ICING_LOG(ERROR) << "ERROR: GetEnv failed"; + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeCreate( + JNIEnv* env, jobject obj, jbyteArray icing_search_engine_options_bytes) { + icing::lib::IcingSearchEngineOptions options; + if (!ParseProtoFromJniByteArray(env, icing_search_engine_options_bytes, + &options)) { + ICING_LOG(ERROR) + << "Failed to parse IcingSearchEngineOptions in nativeCreate"; + return 0; + } + + icing::lib::IcingSearchEngine* icing = + new icing::lib::IcingSearchEngine(options); + return reinterpret_cast<jlong>(icing); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeInitialize( + JNIEnv* env, jobject obj, jlong native_pointer) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::InitializeResultProto initialize_result_proto = + icing->Initialize(); + + return SerializeProtoToJniByteArray(env, initialize_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeSetSchema( + JNIEnv* env, jobject obj, jlong native_pointer, jbyteArray schema_bytes, + jboolean ignore_errors_and_delete_documents) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::SchemaProto schema_proto; + if (!ParseProtoFromJniByteArray(env, schema_bytes, &schema_proto)) { + ICING_LOG(ERROR) << "Failed to parse SchemaProto in nativeSetSchema"; + return nullptr; + } + + icing::lib::SetSchemaResultProto set_schema_result_proto = + icing->SetSchema(schema_proto, ignore_errors_and_delete_documents); + + return SerializeProtoToJniByteArray(env, set_schema_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeGetSchema( + JNIEnv* env, jobject obj, jlong native_pointer) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::GetSchemaResultProto get_schema_result_proto = icing->GetSchema(); + + return SerializeProtoToJniByteArray(env, get_schema_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeGetSchemaType( + JNIEnv* env, jobject obj, jlong native_pointer, jstring schema_type) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + const char* native_schema_type = + env->GetStringUTFChars(schema_type, /*isCopy=*/nullptr); + icing::lib::GetSchemaTypeResultProto get_schema_type_result_proto = + icing->GetSchemaType(native_schema_type); + + return SerializeProtoToJniByteArray(env, get_schema_type_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativePut( + JNIEnv* env, jobject obj, jlong native_pointer, jbyteArray document_bytes) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::DocumentProto document_proto; + if (!ParseProtoFromJniByteArray(env, document_bytes, &document_proto)) { + ICING_LOG(ERROR) << "Failed to parse DocumentProto in nativePut"; + return nullptr; + } + + icing::lib::PutResultProto put_result_proto = icing->Put(document_proto); + + return SerializeProtoToJniByteArray(env, put_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeGet(JNIEnv* env, + jobject obj, + jlong native_pointer, + jstring name_space, + jstring uri) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + const char* native_name_space = + env->GetStringUTFChars(name_space, /*isCopy=*/nullptr); + const char* native_uri = env->GetStringUTFChars(uri, /*isCopy=*/nullptr); + icing::lib::GetResultProto get_result_proto = + icing->Get(native_name_space, native_uri); + + return SerializeProtoToJniByteArray(env, get_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeSearch( + JNIEnv* env, jobject obj, jlong native_pointer, + jbyteArray search_spec_bytes, jbyteArray scoring_spec_bytes, + jbyteArray result_spec_bytes) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::SearchSpecProto search_spec_proto; + if (!ParseProtoFromJniByteArray(env, search_spec_bytes, &search_spec_proto)) { + ICING_LOG(ERROR) << "Failed to parse SearchSpecProto in nativeSearch"; + return nullptr; + } + + icing::lib::ScoringSpecProto scoring_spec_proto; + if (!ParseProtoFromJniByteArray(env, scoring_spec_bytes, + &scoring_spec_proto)) { + ICING_LOG(ERROR) << "Failed to parse ScoringSpecProto in nativeSearch"; + return nullptr; + } + + icing::lib::ResultSpecProto result_spec_proto; + if (!ParseProtoFromJniByteArray(env, result_spec_bytes, &result_spec_proto)) { + ICING_LOG(ERROR) << "Failed to parse ResultSpecProto in nativeSearch"; + return nullptr; + } + + icing::lib::SearchResultProto search_result_proto = + icing->Search(search_spec_proto, scoring_spec_proto, result_spec_proto); + + return SerializeProtoToJniByteArray(env, search_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeDelete( + JNIEnv* env, jobject obj, jlong native_pointer, jstring name_space, + jstring uri) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + const char* native_name_space = + env->GetStringUTFChars(name_space, /*isCopy=*/nullptr); + const char* native_uri = env->GetStringUTFChars(uri, /*isCopy=*/nullptr); + icing::lib::DeleteResultProto delete_result_proto = + icing->Delete(native_name_space, native_uri); + + return SerializeProtoToJniByteArray(env, delete_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeDeleteByNamespace( + JNIEnv* env, jobject obj, jlong native_pointer, jstring name_space) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + const char* native_name_space = + env->GetStringUTFChars(name_space, /*isCopy=*/nullptr); + icing::lib::DeleteByNamespaceResultProto delete_by_namespace_result_proto = + icing->DeleteByNamespace(native_name_space); + + return SerializeProtoToJniByteArray(env, delete_by_namespace_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeDeleteBySchemaType( + JNIEnv* env, jobject obj, jlong native_pointer, jstring schema_type) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + const char* native_schema_type = + env->GetStringUTFChars(schema_type, /*isCopy=*/nullptr); + icing::lib::DeleteBySchemaTypeResultProto delete_by_schema_type_result_proto = + icing->DeleteBySchemaType(native_schema_type); + + return SerializeProtoToJniByteArray(env, delete_by_schema_type_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativePersistToDisk( + JNIEnv* env, jobject obj, jlong native_pointer) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::PersistToDiskResultProto persist_to_disk_result_proto = + icing->PersistToDisk(); + + return SerializeProtoToJniByteArray(env, persist_to_disk_result_proto); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_google_android_icing_IcingSearchEngine_nativeOptimize( + JNIEnv* env, jobject obj, jlong native_pointer) { + icing::lib::IcingSearchEngine* icing = + GetIcingSearchEnginePointer(native_pointer); + + icing::lib::OptimizeResultProto optimize_result_proto = icing->Optimize(); + + return SerializeProtoToJniByteArray(env, optimize_result_proto); +} + +} // extern "C" diff --git a/icing/jni.lds b/icing/jni.lds new file mode 100644 index 0000000..401682a --- /dev/null +++ b/icing/jni.lds @@ -0,0 +1,10 @@ +VERS_1.0 { + # Export JNI symbols. + global: + Java_*; + JNI_OnLoad; + + # Hide everything else + local: + *; +}; diff --git a/java/Android.bp b/java/Android.bp new file mode 100644 index 0000000..6bc8836 --- /dev/null +++ b/java/Android.bp @@ -0,0 +1,22 @@ +// 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. + +java_library { + name: "libicing-java", + srcs: ["src/**/*.java"], + static_libs: [ + "icing-java-proto-lite", + "libprotobuf-java-lite", + ], +} diff --git a/java/src/com/google/android/icing/IcingSearchEngine.java b/java/src/com/google/android/icing/IcingSearchEngine.java new file mode 100644 index 0000000..03a4fbe --- /dev/null +++ b/java/src/com/google/android/icing/IcingSearchEngine.java @@ -0,0 +1,347 @@ +/* + * 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 com.google.android.icing; + +import android.annotation.NonNull; +import android.util.Log; + +import com.google.android.icing.proto.DeleteByNamespaceResultProto; +import com.google.android.icing.proto.DeleteBySchemaTypeResultProto; +import com.google.android.icing.proto.DeleteResultProto; +import com.google.android.icing.proto.DocumentProto; +import com.google.android.icing.proto.GetResultProto; +import com.google.android.icing.proto.GetSchemaResultProto; +import com.google.android.icing.proto.GetSchemaTypeResultProto; +import com.google.android.icing.proto.IcingSearchEngineOptions; +import com.google.android.icing.proto.InitializeResultProto; +import com.google.android.icing.proto.OptimizeResultProto; +import com.google.android.icing.proto.PersistToDiskResultProto; +import com.google.android.icing.proto.PutResultProto; +import com.google.android.icing.proto.ResultSpecProto; +import com.google.android.icing.proto.SchemaProto; +import com.google.android.icing.proto.ScoringSpecProto; +import com.google.android.icing.proto.SearchResultProto; +import com.google.android.icing.proto.SearchSpecProto; +import com.google.android.icing.proto.SetSchemaResultProto; +import com.google.android.icing.proto.StatusProto; +import com.google.android.icing.protobuf.InvalidProtocolBufferException; + +/** Java wrapper to access native APIs in external/icing/icing/icing-search-engine.h */ +public final class IcingSearchEngine { + + private static final String TAG = "IcingSearchEngine"; + + private long mNativePointer; + + static { + // NOTE: This can fail with an UnsatisfiedLinkError + System.loadLibrary("icing_jni"); + } + + /** + * @throws RuntimeException if IcingSearchEngine fails to be created + */ + public IcingSearchEngine(IcingSearchEngineOptions options) { + mNativePointer = nativeCreate(options.toByteArray()); + if (mNativePointer == 0) { + Log.e(TAG, "Failed to create IcingSearchEngine."); + throw new RuntimeException("Failed to create IcingSearchEngine."); + } + } + + + @NonNull + public InitializeResultProto initialize() { + byte[] initializeResultBytes = nativeInitialize(mNativePointer); + if (initializeResultBytes == null) { + Log.e(TAG, "Received null InitializeResult from native."); + return InitializeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return InitializeResultProto.parseFrom(initializeResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing InitializeResultProto.", e); + return InitializeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public SetSchemaResultProto setSchema(@NonNull SchemaProto schema) { + return setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + } + + @NonNull + public SetSchemaResultProto setSchema( + @NonNull SchemaProto schema, boolean ignoreErrorsAndDeleteDocuments) { + byte[] setSchemaResultBytes = + nativeSetSchema(mNativePointer, schema.toByteArray(), ignoreErrorsAndDeleteDocuments); + if (setSchemaResultBytes == null) { + Log.e(TAG, "Received null SetSchemaResultProto from native."); + return SetSchemaResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return SetSchemaResultProto.parseFrom(setSchemaResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing SetSchemaResultProto.", e); + return SetSchemaResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public GetSchemaResultProto getSchema() { + byte[] getSchemaResultBytes = nativeGetSchema(mNativePointer); + if (getSchemaResultBytes == null) { + Log.e(TAG, "Received null GetSchemaResultProto from native."); + return GetSchemaResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return GetSchemaResultProto.parseFrom(getSchemaResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing GetSchemaResultProto.", e); + return GetSchemaResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public GetSchemaTypeResultProto getSchemaType(@NonNull String schemaType) { + byte[] getSchemaTypeResultBytes = nativeGetSchemaType(mNativePointer, schemaType); + if (getSchemaTypeResultBytes == null) { + Log.e(TAG, "Received null GetSchemaTypeResultProto from native."); + return GetSchemaTypeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return GetSchemaTypeResultProto.parseFrom(getSchemaTypeResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing GetSchemaTypeResultProto.", e); + return GetSchemaTypeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public PutResultProto put(@NonNull DocumentProto document) { + byte[] putResultBytes = nativePut(mNativePointer, document.toByteArray()); + if (putResultBytes == null) { + Log.e(TAG, "Received null PutResultProto from native."); + return PutResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return PutResultProto.parseFrom(putResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing PutResultProto.", e); + return PutResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public GetResultProto get(@NonNull String namespace, @NonNull String uri) { + byte[] getResultBytes = nativeGet(mNativePointer, namespace, uri); + if (getResultBytes == null) { + Log.e(TAG, "Received null GetResultProto from native."); + return GetResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return GetResultProto.parseFrom(getResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing GetResultProto.", e); + return GetResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public SearchResultProto search( + @NonNull SearchSpecProto searchSpec, @NonNull ScoringSpecProto scoringSpec, @NonNull ResultSpecProto resultSpec) { + byte[] searchResultBytes = + nativeSearch( + mNativePointer, + searchSpec.toByteArray(), + scoringSpec.toByteArray(), + resultSpec.toByteArray()); + if (searchResultBytes == null) { + Log.e(TAG, "Received null SearchResultProto from native."); + return SearchResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return SearchResultProto.parseFrom(searchResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing SearchResultProto.", e); + return SearchResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public DeleteResultProto delete(@NonNull String namespace, @NonNull String uri) { + byte[] deleteResultBytes = nativeDelete(mNativePointer, namespace, uri); + if (deleteResultBytes == null) { + Log.e(TAG, "Received null DeleteResultProto from native."); + return DeleteResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return DeleteResultProto.parseFrom(deleteResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing DeleteResultProto.", e); + return DeleteResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public DeleteByNamespaceResultProto deleteByNamespace(@NonNull String namespace) { + byte[] deleteByNamespaceResultBytes = nativeDeleteByNamespace(mNativePointer, namespace); + if (deleteByNamespaceResultBytes == null) { + Log.e(TAG, "Received null DeleteByNamespaceResultProto from native."); + return DeleteByNamespaceResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return DeleteByNamespaceResultProto.parseFrom(deleteByNamespaceResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing DeleteByNamespaceResultProto.", e); + return DeleteByNamespaceResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public DeleteBySchemaTypeResultProto deleteBySchemaType(@NonNull String schemaType) { + byte[] deleteBySchemaTypeResultBytes = nativeDeleteBySchemaType(mNativePointer, schemaType); + if (deleteBySchemaTypeResultBytes == null) { + Log.e(TAG, "Received null DeleteBySchemaTypeResultProto from native."); + return DeleteBySchemaTypeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return DeleteBySchemaTypeResultProto.parseFrom(deleteBySchemaTypeResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing DeleteBySchemaTypeResultProto.", e); + return DeleteBySchemaTypeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public PersistToDiskResultProto persistToDisk() { + byte[] persistToDiskResultBytes = nativePersistToDisk(mNativePointer); + if (persistToDiskResultBytes == null) { + Log.e(TAG, "Received null PersistToDiskResultProto from native."); + return PersistToDiskResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return PersistToDiskResultProto.parseFrom(persistToDiskResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing PersistToDiskResultProto.", e); + return PersistToDiskResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + @NonNull + public OptimizeResultProto optimize() { + byte[] optimizeResultBytes = nativeOptimize(mNativePointer); + if (optimizeResultBytes == null) { + Log.e(TAG, "Received null OptimizeResultProto from native."); + return OptimizeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + + try { + return OptimizeResultProto.parseFrom(optimizeResultBytes); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Error parsing OptimizeResultProto.", e); + return OptimizeResultProto.newBuilder() + .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL)) + .build(); + } + } + + private static native long nativeCreate(byte[] icingSearchEngineOptionsBytes); + + private static native byte[] nativeInitialize(long mNativePointer); + + private static native byte[] nativeSetSchema( + long mNativePointer, byte[] schemaBytes, boolean ignoreErrorsAndDeleteDocuments); + + private static native byte[] nativeGetSchema(long mNativePointer); + + private static native byte[] nativeGetSchemaType(long mNativePointer, String schemaType); + + private static native byte[] nativePut(long mNativePointer, byte[] documentBytes); + + private static native byte[] nativeGet(long mNativePointer, String namespace, String uri); + + private static native byte[] nativeSearch( + long mNativePointer, byte[] searchSpecBytes, byte[] scoringSpecBytes, byte[] resultSpecBytes); + + private static native byte[] nativeDelete(long mNativePointer, String namespace, String uri); + + private static native byte[] nativeDeleteByNamespace(long mNativePointer, String namespace); + + private static native byte[] nativeDeleteBySchemaType(long mNativePointer, String schemaType); + + private static native byte[] nativePersistToDisk(long mNativePointer); + + private static native byte[] nativeOptimize(long mNativePointer); +} diff --git a/java/tests/instrumentation/Android.bp b/java/tests/instrumentation/Android.bp new file mode 100644 index 0000000..c941acf --- /dev/null +++ b/java/tests/instrumentation/Android.bp @@ -0,0 +1,42 @@ +// Copyright (C) 2019 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. + +android_test { + name: "IcingSearchEngineTest", + + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.ext.junit", + "androidx.test.rules", + "androidx.test.ext.truth", + "libicing-java", + "icing-java-proto-lite", + ], + + jni_libs: [ + "libicing_jni", + ], + + test_suites: [ + "device-tests", + ], + + platform_apis: true, + use_embedded_native_libs: true, +} diff --git a/java/tests/instrumentation/AndroidManifest.xml b/java/tests/instrumentation/AndroidManifest.xml new file mode 100644 index 0000000..790e5da --- /dev/null +++ b/java/tests/instrumentation/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.icing"> + + <uses-sdk android:minSdkVersion="28"/> + + <application> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.google.android.icing"/> +</manifest> diff --git a/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java b/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java new file mode 100644 index 0000000..5c502d9 --- /dev/null +++ b/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java @@ -0,0 +1,284 @@ +/* + * 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 com.google.android.icing; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; + +import com.google.android.icing.proto.DeleteByNamespaceResultProto; +import com.google.android.icing.proto.DeleteBySchemaTypeResultProto; +import com.google.android.icing.proto.DeleteResultProto; +import com.google.android.icing.proto.DocumentProto; +import com.google.android.icing.proto.GetResultProto; +import com.google.android.icing.proto.GetSchemaResultProto; +import com.google.android.icing.proto.GetSchemaTypeResultProto; +import com.google.android.icing.proto.IcingSearchEngineOptions; +import com.google.android.icing.proto.IndexingConfig; +import com.google.android.icing.proto.IndexingConfig.TokenizerType; +import com.google.android.icing.proto.InitializeResultProto; +import com.google.android.icing.proto.OptimizeResultProto; +import com.google.android.icing.proto.PersistToDiskResultProto; +import com.google.android.icing.proto.PropertyConfigProto; +import com.google.android.icing.proto.PropertyProto; +import com.google.android.icing.proto.PutResultProto; +import com.google.android.icing.proto.ResultSpecProto; +import com.google.android.icing.proto.SchemaProto; +import com.google.android.icing.proto.SchemaTypeConfigProto; +import com.google.android.icing.proto.ScoringSpecProto; +import com.google.android.icing.proto.SearchResultProto; +import com.google.android.icing.proto.SearchSpecProto; +import com.google.android.icing.proto.SetSchemaResultProto; +import com.google.android.icing.proto.StatusProto; +import com.google.android.icing.proto.TermMatchType; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * This test is not intended to fully test the functionality of each API. But rather to test the JNI + * wrapper of Icing library. + */ +@RunWith(JUnit4.class) +public final class IcingSearchEngineTest { + + private static final String EMAIL_TYPE = "Email"; + + private Context mContext; + private String mFilesDir; + + static SchemaTypeConfigProto createEmailTypeConfig() { + return SchemaTypeConfigProto.newBuilder() + .setSchemaType(EMAIL_TYPE) + .addProperties( + PropertyConfigProto.newBuilder() + .setPropertyName("subject") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX))) + .addProperties( + PropertyConfigProto.newBuilder() + .setPropertyName("body") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX))) + .build(); + } + + static DocumentProto createEmailDocument(String namespace, String uri) { + return DocumentProto.newBuilder() + .setNamespace(namespace) + .setUri(uri) + .setSchema(EMAIL_TYPE) + .setCreationTimestampMs(1) // Arbitrary non-zero number so Icing doesn't override it + .build(); + } + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mFilesDir = mContext.getFilesDir().getCanonicalPath(); + } + + @Test + public void testInitialize() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + + InitializeResultProto initializeResultProto = icing.initialize(); + assertThat(initializeResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + } + + @Test + public void testSetAndGetSchema() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + assertThat(setSchemaResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + + GetSchemaResultProto getSchemaResultProto = icing.getSchema(); + assertThat(getSchemaResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + assertThat(getSchemaResultProto.getSchema()).isEqualTo(schema); + + GetSchemaTypeResultProto getSchemaTypeResultProto = + icing.getSchemaType(emailTypeConfig.getSchemaType()); + assertThat(getSchemaTypeResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + assertThat(getSchemaTypeResultProto.getSchemaTypeConfig()).isEqualTo(emailTypeConfig); + } + + @Test + public void testPutAndGetDocuments() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + + DocumentProto emailDocument = createEmailDocument("namespace", "uri"); + PutResultProto putResultProto = icing.put(emailDocument); + assertThat(putResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + + GetResultProto getResultProto = icing.get("namespace", "uri"); + assertThat(getResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + assertThat(getResultProto.getDocument()).isEqualTo(emailDocument); + } + + @Test + public void testSearch() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + + DocumentProto emailDocument = + createEmailDocument("namespace", "uri").toBuilder() + .addProperties(PropertyProto.newBuilder().setName("subject").addStringValues("foo")) + .build(); + PutResultProto putResultProto = icing.put(emailDocument); + + SearchSpecProto searchSpec = + SearchSpecProto.newBuilder() + .setQuery("foo") + .setTermMatchType(TermMatchType.Code.PREFIX) + .build(); + + SearchResultProto searchResultProto = + icing.search( + searchSpec, + ScoringSpecProto.getDefaultInstance(), + ResultSpecProto.getDefaultInstance()); + assertThat(searchResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + assertThat(searchResultProto.getResultsCount()).isEqualTo(1); + assertThat(searchResultProto.getResults(0).getDocument()).isEqualTo(emailDocument); + } + + @Test + public void testDelete() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + + DocumentProto emailDocument = createEmailDocument("namespace", "uri"); + PutResultProto putResultProto = icing.put(emailDocument); + + DeleteResultProto deleteResultProto = icing.delete("namespace", "uri"); + assertThat(deleteResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + + GetResultProto getResultProto = icing.get("namespace", "uri"); + assertThat(getResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.NOT_FOUND); + } + + @Test + public void testDeleteByNamespace() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + + DocumentProto emailDocument = createEmailDocument("namespace", "uri"); + PutResultProto putResultProto = icing.put(emailDocument); + + DeleteByNamespaceResultProto deleteByNamespaceResultProto = + icing.deleteByNamespace("namespace"); + assertThat(deleteByNamespaceResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + + GetResultProto getResultProto = icing.get("namespace", "uri"); + assertThat(getResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.NOT_FOUND); + } + + @Test + public void testDeleteBySchemaType() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig(); + SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build(); + SetSchemaResultProto setSchemaResultProto = + icing.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false); + + DocumentProto emailDocument = createEmailDocument("namespace", "uri"); + PutResultProto putResultProto = icing.put(emailDocument); + + DeleteBySchemaTypeResultProto deleteBySchemaTypeResultProto = + icing.deleteBySchemaType(EMAIL_TYPE); + assertThat(deleteBySchemaTypeResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + + GetResultProto getResultProto = icing.get("namespace", "uri"); + assertThat(getResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.NOT_FOUND); + } + + @Test + public void testPersistToDisk() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + PersistToDiskResultProto persistToDiskResultProto = icing.persistToDisk(); + assertThat(persistToDiskResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + } + + @Test + public void testOptimize() throws Exception { + IcingSearchEngineOptions options = + IcingSearchEngineOptions.newBuilder().setBaseDir(mFilesDir).build(); + IcingSearchEngine icing = new IcingSearchEngine(options); + InitializeResultProto initializeResultProto = icing.initialize(); + + OptimizeResultProto optimizeResultProto = icing.optimize(); + assertThat(optimizeResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK); + } +} |