blob: d558ae01ec3bce491bcf9fbbde8c405d1f857b43 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.app.appsearch.cts.app;
import static android.Manifest.permission.READ_CALENDAR;
import static android.Manifest.permission.READ_CONTACTS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
import static android.Manifest.permission.READ_SMS;
import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
import static com.google.common.truth.Truth.assertThat;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchSessionShim;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.GetByDocumentIdRequest;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.GlobalSearchSessionShim;
import android.app.appsearch.PackageIdentifier;
import android.app.appsearch.PutDocumentsRequest;
import android.app.appsearch.ReportSystemUsageRequest;
import android.app.appsearch.ReportUsageRequest;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResultsShim;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SetSchemaRequest;
import android.app.appsearch.observer.DocumentChangeInfo;
import android.app.appsearch.observer.ObserverSpec;
import android.app.appsearch.testutil.AppSearchEmail;
import android.app.appsearch.testutil.AppSearchSessionShimImpl;
import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
import android.app.appsearch.testutil.TestObserverCallback;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.platform.test.annotations.AppModeFull;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import com.android.compatibility.common.util.SystemUtil;
import com.android.cts.appsearch.ICommandReceiver;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* This doesn't extend {@link android.app.appsearch.cts.app.GlobalSearchSessionCtsTestBase} since
* these test cases can't be run in a non-platform environment.
*/
@AppModeFull(reason = "Can't bind to helper apps from instant mode")
public class GlobalSearchSessionPlatformCtsTest {
private static final long TIMEOUT_BIND_SERVICE_SEC = 2;
private static final String TAG = "GlobalSearchSessionPlatformCtsTest";
private static final String PKG_A = "com.android.cts.appsearch.helper.a";
// To generate, run `apksigner` on the build APK. e.g.
// ./apksigner verify --print-certs \
// ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperA/\
// android_common/CtsAppSearchTestHelperA.apk`
// to get the SHA-256 digest. All characters need to be uppercase.
//
// Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
// building the apk and running apksigner
private static final byte[] PKG_A_CERT_SHA256 =
BaseEncoding.base16()
.decode("A90B80BD307B71BB4029674C5C4FE18066994E352EAC933B7B68266210CAFB53");
private static final String PKG_B = "com.android.cts.appsearch.helper.b";
// To generate, run `apksigner` on the build APK. e.g.
// ./apksigner verify --print-certs \
// ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperB/\
// android_common/CtsAppSearchTestHelperB.apk`
// to get the SHA-256 digest. All characters need to be uppercase.
//
// Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
// building the apk and running apksigner
private static final byte[] PKG_B_CERT_SHA256 =
BaseEncoding.base16()
.decode("88C0B41A31943D13226C3F22A86A6B4F300315575A6BC533CBF16C4EF3CFAA37");
private static final String HELPER_SERVICE =
"com.android.cts.appsearch.helper.AppSearchTestService";
private static final String TEXT = "foo";
private static final String NAMESPACE_NAME = "namespace";
private static final AppSearchEmail EMAIL_DOCUMENT =
new AppSearchEmail.Builder(NAMESPACE_NAME, "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject(TEXT)
.setBody("this is the body of the email")
.build();
private static final String DB_NAME = "database";
private GlobalSearchSessionShim mGlobalSearchSession;
private AppSearchSessionShim mDb;
private Context mContext;
@Before
public void setUp() throws Exception {
mContext = ApplicationProvider.getApplicationContext();
mDb =
AppSearchSessionShimImpl.createSearchSessionAsync(
new AppSearchManager.SearchContext.Builder(DB_NAME).build())
.get();
cleanup();
}
@After
public void tearDown() throws Exception {
// Cleanup whatever documents may still exist in these databases.
cleanup();
}
private void cleanup() throws Exception {
mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
clearData(PKG_A, DB_NAME);
clearData(PKG_B, DB_NAME);
}
@Test
public void testNoPackageAccess_default() throws Exception {
mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
// No package has access by default
assertPackageCannotAccess(PKG_A);
assertPackageCannotAccess(PKG_B);
}
@Test
public void testNoPackageAccess_wrongPackageName() throws Exception {
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(
"some.other.package", PKG_A_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCannotAccess(PKG_A);
}
@Test
public void testNoPackageAccess_wrongCertificate() throws Exception {
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, new byte[] {10}))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCannotAccess(PKG_A);
}
@Test
public void testAllowPackageAccess() throws Exception {
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
assertPackageCannotAccess(PKG_B);
}
@Test
public void testAllowMultiplePackageAccess() throws Exception {
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
assertPackageCanAccess(EMAIL_DOCUMENT, PKG_B);
}
@Test
public void testNoPackageAccess_revoked() throws Exception {
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
// Set the schema again, but package access as false.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ false,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCannotAccess(PKG_A);
// Set the schema again, but with default (i.e. no) access
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ false,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(EMAIL_DOCUMENT)
.build()));
assertPackageCannotAccess(PKG_A);
}
@Test
public void testAllowPermissionAccess() throws Exception {
// index a global searchable document in pkg_A and set it needs READ_SMS to read it.
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).hasSize(1);
},
READ_SMS);
}
//TODO(b/202194495) add test for READ_HOME_APP_SEARCH_DATA and READ_ASSISTANT_APP_SEARCH_DATA
// once they are available in Shell.
@Test
public void testRequireAllPermissionsOfAnyCombinationToAccess() throws Exception {
// index a global searchable document in pkg_A and set it needs both READ_SMS and
// READ_CALENDAR or READ_HOME_APP_SEARCH_DATA only or READ_ASSISTANT_APP_SEARCH_DATA
// only to read it.
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_SMS,
SetSchemaRequest.READ_CALENDAR),
ImmutableSet.of(SetSchemaRequest.READ_CONTACTS),
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)));
// Has READ_SMS only cannot access the document.
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).isEmpty();
},
READ_SMS);
// Has READ_SMS and READ_CALENDAR can access the document.
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).hasSize(1);
},
READ_SMS, READ_CALENDAR);
// Has READ_CONTACTS can access the document also.
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).hasSize(1);
},
READ_CONTACTS);
// Has READ_EXTERNAL_STORAGE can access the document.
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).hasSize(1);
},
READ_EXTERNAL_STORAGE);
}
@Test
public void testAllowPermissions_sameError() throws Exception {
// Try to get document before we put them, this is not found error.
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
AppSearchBatchResult<String, GenericDocument> nonExistentResult = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(nonExistentResult.isSuccess()).isFalse();
assertThat(nonExistentResult.getSuccesses()).isEmpty();
assertThat(nonExistentResult.getFailures()).containsKey("id1");
// Index a global searchable document in pkg_A and set it needs READ_SMS to read it.
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
// Try to get document w/o permission, this is unAuthority error.
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
AppSearchBatchResult<String, GenericDocument> unAuthResult = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, "database",
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build()).get();
assertThat(unAuthResult.isSuccess()).isFalse();
assertThat(unAuthResult.getSuccesses()).isEmpty();
assertThat(unAuthResult.getFailures()).containsKey("id1");
// The error messages must be same.
assertThat(unAuthResult.getFailures().get("id1").getErrorMessage())
.isEqualTo(nonExistentResult.getFailures().get("id1").getErrorMessage());
}
@Test
public void testGlobalGetById_withAccess() throws Exception {
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id1")
.build()).get();
assertThat(result.getSuccesses()).hasSize(1);
// Can't get non existent document
AppSearchBatchResult<String, GenericDocument> nonExistent = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id2")
.build()).get();
assertThat(nonExistent.isSuccess()).isFalse();
assertThat(nonExistent.getSuccesses()).hasSize(0);
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalGetById_withoutAccess() throws Exception {
indexNotGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
// Can't get the document
AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id1")
.build()).get();
assertThat(result.isSuccess()).isFalse();
assertThat(result.getSuccesses()).hasSize(0);
assertThat(result.getFailures()).containsKey("id1");
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalGetById_sameErrorMessages() throws Exception {
AtomicReference<String> errorMessageNonExistent = new AtomicReference<>();
AtomicReference<String> errorMessageUnauth = new AtomicReference<>();
// Can't get the document because we haven't added it yet
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
AppSearchBatchResult<String, GenericDocument> nonExistentResult =
mGlobalSearchSession.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id1")
.build()).get();
assertThat(nonExistentResult.isSuccess()).isFalse();
assertThat(nonExistentResult.getSuccesses()).hasSize(0);
assertThat(nonExistentResult.getFailures()).containsKey("id1");
errorMessageNonExistent.set(
nonExistentResult.getFailures().get("id1").getErrorMessage());
},
READ_GLOBAL_APP_SEARCH_DATA);
indexNotGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
// Can't get the document because the document is not globally searchable
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
AppSearchBatchResult<String, GenericDocument> unAuthResult =
mGlobalSearchSession.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id1")
.build()).get();
assertThat(unAuthResult.isSuccess()).isFalse();
assertThat(unAuthResult.getSuccesses()).hasSize(0);
assertThat(unAuthResult.getFailures()).containsKey("id1");
errorMessageUnauth.set(
unAuthResult.getFailures().get("id1").getErrorMessage());
},
READ_GLOBAL_APP_SEARCH_DATA);
// try adding a global doc here to make sure non-global querier can't get it
// and same error message
clearData(PKG_A, DB_NAME);
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
// Can't get the document because we don't have global permissions
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
AppSearchBatchResult<String, GenericDocument> noGlobalResult = mGlobalSearchSession
.getByDocumentIdAsync(PKG_A, DB_NAME,
new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
.addIds("id1")
.build()).get();
assertThat(noGlobalResult.isSuccess()).isFalse();
assertThat(noGlobalResult.getSuccesses()).hasSize(0);
assertThat(noGlobalResult.getFailures()).containsKey("id1");
// compare error messages
assertThat(errorMessageNonExistent.get()).isEqualTo(errorMessageUnauth.get());
assertThat(errorMessageNonExistent.get())
.isEqualTo(noGlobalResult.getFailures().get("id1").getErrorMessage());
}
@Test
public void testGlobalSearch_withAccess() throws Exception {
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
SearchResultsShim searchResults =
mGlobalSearchSession.search(
/*queryExpression=*/ "",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames(PKG_A, PKG_B)
.build());
List<SearchResult> page = searchResults.getNextPage().get();
assertThat(page).hasSize(2);
Set<String> actualPackageNames =
ImmutableSet.of(
page.get(0).getPackageName(), page.get(1).getPackageName());
assertThat(actualPackageNames).containsExactly(PKG_A, PKG_B);
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalSearch_withPartialAccess() throws Exception {
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexNotGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
SearchResultsShim searchResults =
mGlobalSearchSession.search(
/*queryExpression=*/ "",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames(PKG_A, PKG_B)
.build());
List<SearchResult> page = searchResults.getNextPage().get();
assertThat(page).hasSize(1);
assertThat(page.get(0).getPackageName()).isEqualTo(PKG_A);
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalSearch_withPackageFilters() throws Exception {
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext)
.get();
SearchResultsShim searchResults =
mGlobalSearchSession.search(
/*queryExpression=*/ "",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames(PKG_B)
.build());
List<SearchResult> page = searchResults.getNextPage().get();
assertThat(page).hasSize(1);
assertThat(page.get(0).getPackageName()).isEqualTo(PKG_B);
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalSearch_withoutAccess() throws Exception {
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
SearchResultsShim searchResults =
mGlobalSearchSession.search(
/*queryExpression=*/ "",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames(PKG_A, PKG_B)
.build());
List<SearchResult> page = searchResults.getNextPage().get();
assertThat(page).isEmpty();
}
@Test
public void testGlobalGetSchema_packageAccess_defaultAccess() throws Exception {
// 1. Create a schema in the test with default (no) access.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build())
.get();
// 2. Neither PKG_A nor PKG_B should be able to retrieve the schema.
List<String> schemaStrings = getSchemaAsPackage(PKG_A);
assertThat(schemaStrings).isNull();
schemaStrings = getSchemaAsPackage(PKG_B);
assertThat(schemaStrings).isNull();
}
@Test
public void testGlobalGetSchema_packageAccess_singleAccess() throws Exception {
// 1. Create a schema in the test with access granted to PKG_A, but not PKG_B.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
// 2. Only PKG_A should be able to retrieve the schema.
List<String> schemaStrings = getSchemaAsPackage(PKG_A);
assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
schemaStrings = getSchemaAsPackage(PKG_B);
assertThat(schemaStrings).isNull();
}
@Test
public void testGlobalGetSchema_packageAccess_multiAccess() throws Exception {
// 1. Create a schema in the test with access granted to PKG_A and PKG_B.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
.build())
.get();
// 2. Both packages should be able to retrieve the schema.
List<String> schemaStrings = getSchemaAsPackage(PKG_A);
assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
schemaStrings = getSchemaAsPackage(PKG_B);
assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
}
@Test
public void testGlobalGetSchema_packageAccess_revokeAccess() throws Exception {
// 1. Create a schema in the test with access granted to PKG_A.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ true,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
// 2. Now revoke that access.
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeVisibilityForPackage(
AppSearchEmail.SCHEMA_TYPE,
/*visible=*/ false,
new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
.build())
.get();
// 3. PKG_A should NOT be able to retrieve the schema.
List<String> schemaStrings = getSchemaAsPackage(PKG_A);
assertThat(schemaStrings).isNull();
}
@Test
public void testGlobalGetSchema_globalAccess_singleAccess() throws Exception {
// 1. Index documents for PKG_A and PKG_B. This will set the schema for each with the
// corresponding access set.
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexNotGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(
mContext).get();
// 2. The schema for PKG_A should be retrievable, but PKG_B should not be.
GetSchemaResponse response =
mGlobalSearchSession.getSchema(PKG_A, DB_NAME).get();
assertThat(response.getSchemas()).hasSize(1);
response = mGlobalSearchSession.getSchema(PKG_B, DB_NAME).get();
assertThat(response.getSchemas()).isEmpty();
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testGlobalGetSchema_globalAccess_multiAccess() throws Exception {
// 1. Index documents for PKG_A and PKG_B. This will set the schema for each with the
// corresponding access set.
indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
SystemUtil.runWithShellPermissionIdentity(
() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(
mContext).get();
// 2. The schema for both PKG_A and PKG_B should be retrievable.
GetSchemaResponse response =
mGlobalSearchSession.getSchema(PKG_A, DB_NAME).get();
assertThat(response.getSchemas()).hasSize(1);
response = mGlobalSearchSession.getSchema(PKG_B, DB_NAME).get();
assertThat(response.getSchemas()).hasSize(1);
},
READ_GLOBAL_APP_SEARCH_DATA);
}
@Test
public void testReportSystemUsage() throws Exception {
// Insert schema
mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Insert two docs
GenericDocument document1 =
new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", AppSearchEmail.SCHEMA_TYPE)
.build();
GenericDocument document2 =
new GenericDocument.Builder<>(NAMESPACE_NAME, "id2", AppSearchEmail.SCHEMA_TYPE)
.build();
mDb.put(new PutDocumentsRequest.Builder().addGenericDocuments(document1, document2).build())
.get();
// Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
mDb.reportUsage(
new ReportUsageRequest.Builder(NAMESPACE_NAME, "id1")
.setUsageTimestampMillis(10)
.build())
.get();
mDb.reportUsage(
new ReportUsageRequest.Builder(NAMESPACE_NAME, "id1")
.setUsageTimestampMillis(20)
.build())
.get();
mDb.reportUsage(
new ReportUsageRequest.Builder(NAMESPACE_NAME, "id2")
.setUsageTimestampMillis(100)
.build())
.get();
SystemUtil.runWithShellPermissionIdentity(() -> {
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
mGlobalSearchSession
.reportSystemUsage(
new ReportSystemUsageRequest.Builder(
mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id1")
.setUsageTimestampMillis(1000)
.build())
.get();
mGlobalSearchSession
.reportSystemUsage(
new ReportSystemUsageRequest.Builder(
mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id2")
.setUsageTimestampMillis(200)
.build())
.get();
mGlobalSearchSession
.reportSystemUsage(
new ReportSystemUsageRequest.Builder(
mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id2")
.setUsageTimestampMillis(150)
.build())
.get();
}, READ_GLOBAL_APP_SEARCH_DATA);
// Query the data
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
// Sort by app usage count: id1 should win
try (SearchResultsShim results = mDb.search(
"",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
.build())) {
List<SearchResult> page = results.getNextPage().get();
assertThat(page).hasSize(2);
assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
}
// Sort by app usage timestamp: id2 should win
try (SearchResultsShim results = mDb.search(
"",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
.build())) {
List<SearchResult> page = results.getNextPage().get();
assertThat(page).hasSize(2);
assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
}
// Sort by system usage count: id2 should win
try (SearchResultsShim results = mDb.search(
"",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy(
SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
.build())) {
List<SearchResult> page = results.getNextPage().get();
assertThat(page).hasSize(2);
assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
}
// Sort by system usage timestamp: id1 should win
SearchSpec searchSpec = new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy(SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
.build();
try (SearchResultsShim results = mDb.search("", searchSpec)) {
List<SearchResult> page = results.getNextPage().get();
assertThat(page).hasSize(2);
assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
}
}
@Test
public void testRemoveObserver_otherPackagesNotRemoved() throws Exception {
final String fakePackage = "com.android.appsearch.fake.package";
TestObserverCallback observer = new TestObserverCallback();
// Set up schema
mGlobalSearchSession =
GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
mDb.setSchema(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Register this observer twice, on different packages.
Executor executor = MoreExecutors.directExecutor();
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
executor,
observer);
mGlobalSearchSession.registerObserverCallback(
/*observedPackage=*/fakePackage,
new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
executor,
observer);
// Make sure everything is empty
assertThat(observer.getSchemaChanges()).isEmpty();
assertThat(observer.getDocumentChanges()).isEmpty();
// Index some documents
AppSearchEmail email1 = new AppSearchEmail.Builder(NAMESPACE_NAME, "id1").build();
AppSearchEmail email2 =
new AppSearchEmail.Builder(NAMESPACE_NAME, "id2").setBody("caterpillar").build();
AppSearchEmail email3 =
new AppSearchEmail.Builder(NAMESPACE_NAME, "id3").setBody("foo").build();
checkIsBatchResultSuccess(
mDb.put(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1).build()));
// Make sure the notifications were received.
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges()).isEmpty();
assertThat(observer.getDocumentChanges()).containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME,
NAMESPACE_NAME,
AppSearchEmail.SCHEMA_TYPE,
ImmutableSet.of("id1")));
observer.clear();
// Unregister observer from com.example.package
mGlobalSearchSession.unregisterObserverCallback("com.example.package", observer);
// Index some more documents
assertThat(observer.getDocumentChanges()).isEmpty();
checkIsBatchResultSuccess(
mDb.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Make sure data was still received
observer.waitForNotificationCount(1);
assertThat(observer.getDocumentChanges()).containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME,
NAMESPACE_NAME,
AppSearchEmail.SCHEMA_TYPE,
ImmutableSet.of("id2")));
observer.clear();
// Unregister the final observer
mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
// Index some more documents
assertThat(observer.getDocumentChanges()).isEmpty();
checkIsBatchResultSuccess(
mDb.put(new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()));
// Make sure there have been no further notifications
assertThat(observer.getDocumentChanges()).isEmpty();
}
private List<String> getSchemaAsPackage(String pkg) throws Exception {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
return commandReceiver.globalGetSchema(mContext.getPackageName(), DB_NAME);
} finally {
serviceConnection.unbind();
}
}
private void assertPackageCannotAccess(String pkg) throws Exception {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
List<String> results = commandReceiver.globalSearch(TEXT);
assertThat(results).isEmpty();
} finally {
serviceConnection.unbind();
}
}
private void assertPackageCanAccess(GenericDocument expectedDocument, String pkg)
throws Exception {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
List<String> results = commandReceiver.globalSearch(TEXT);
assertThat(results).containsExactly(expectedDocument.toString());
} finally {
serviceConnection.unbind();
}
}
private void indexGloballySearchableDocument(String pkg, String databaseName, String namespace,
String id) throws Exception {
indexGloballySearchableDocument(pkg, databaseName, namespace, id, Collections.emptySet());
}
private void indexGloballySearchableDocument(String pkg, String databaseName, String namespace,
String id, Set<Set<Integer>> visibleToPermissions) throws Exception {
// binder won't accept Set or Integer, we need to convert to List<Bundle>.
List<Bundle> permissionBundles = new ArrayList<>(visibleToPermissions.size());
for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
Bundle permissionBundle = new Bundle();
permissionBundle.putIntegerArrayList("permission",
new ArrayList<>(allRequiredPermissions));
permissionBundles.add(permissionBundle);
}
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
assertThat(commandReceiver.indexGloballySearchableDocument(
databaseName, namespace, id, permissionBundles)).isTrue();
} finally {
serviceConnection.unbind();
}
}
private void indexNotGloballySearchableDocument(
String pkg, String databaseName, String namespace, String id) throws Exception {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
assertThat(commandReceiver
.indexNotGloballySearchableDocument(databaseName, namespace, id)).isTrue();
} finally {
serviceConnection.unbind();
}
}
private void clearData(String pkg, String databaseName) throws Exception {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
bindToHelperService(pkg);
try {
ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
assertThat(commandReceiver.clearData(databaseName)).isTrue();
} finally {
serviceConnection.unbind();
}
}
private GlobalSearchSessionPlatformCtsTest.TestServiceConnection bindToHelperService(
String pkg) {
GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
new GlobalSearchSessionPlatformCtsTest.TestServiceConnection(mContext);
Intent intent = new Intent().setComponent(new ComponentName(pkg, HELPER_SERVICE));
mContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
return serviceConnection;
}
private class TestServiceConnection implements ServiceConnection {
private final Context mContext;
private final BlockingQueue<IBinder> mBlockingQueue = new LinkedBlockingQueue<>();
private ICommandReceiver mCommandReceiver;
TestServiceConnection(Context context) {
mContext = context;
}
public void onServiceConnected(ComponentName componentName, IBinder service) {
Log.i(TAG, "Service got connected: " + componentName);
mBlockingQueue.offer(service);
}
public void onServiceDisconnected(ComponentName componentName) {
Log.e(TAG, "Service got disconnected: " + componentName);
}
private IBinder getService() throws Exception {
IBinder service = mBlockingQueue.poll(TIMEOUT_BIND_SERVICE_SEC, TimeUnit.SECONDS);
return service;
}
public ICommandReceiver getCommandReceiver() throws Exception {
if (mCommandReceiver == null) {
mCommandReceiver = ICommandReceiver.Stub.asInterface(getService());
}
return mCommandReceiver;
}
public void unbind() {
mCommandReceiver = null;
mContext.unbindService(this);
}
}
}