Merge changes If2d46362,I2472414b into androidx-main am: 85ea688ec5
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2053546
Change-Id: I12e916314cba242de0e67de33cce72b85c151557
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index e01b95c..176077e 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -65,6 +65,7 @@
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -163,6 +164,21 @@
method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
}
+ public final class PickVisualMediaRequest {
+ method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+ property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+ }
+
+ public static final class PickVisualMediaRequest.Builder {
+ ctor public PickVisualMediaRequest.Builder();
+ method public androidx.activity.result.PickVisualMediaRequest build();
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
+ public final class PickVisualMediaRequestKt {
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
}
package androidx.activity.result.contract {
@@ -239,6 +255,49 @@
method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
}
+ @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+ ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+ }
+
+ public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+ ctor public ActivityResultContracts.PickVisualMedia();
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public static final boolean isPhotoPickerAvailable();
+ method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.Companion {
+ method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public boolean isPhotoPickerAvailable();
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+ method public String getMimeType();
+ property public final String mimeType;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+ }
+
+ public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ }
+
public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
ctor public ActivityResultContracts.RequestMultiplePermissions();
method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index e01b95c..176077e 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -65,6 +65,7 @@
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -163,6 +164,21 @@
method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
}
+ public final class PickVisualMediaRequest {
+ method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+ property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+ }
+
+ public static final class PickVisualMediaRequest.Builder {
+ ctor public PickVisualMediaRequest.Builder();
+ method public androidx.activity.result.PickVisualMediaRequest build();
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
+ public final class PickVisualMediaRequestKt {
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
}
package androidx.activity.result.contract {
@@ -239,6 +255,49 @@
method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
}
+ @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+ ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+ }
+
+ public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+ ctor public ActivityResultContracts.PickVisualMedia();
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public static final boolean isPhotoPickerAvailable();
+ method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.Companion {
+ method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public boolean isPhotoPickerAvailable();
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+ method public String getMimeType();
+ property public final String mimeType;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+ }
+
+ public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ }
+
public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
ctor public ActivityResultContracts.RequestMultiplePermissions();
method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 5fdb966..0a8d8b9 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -64,6 +64,7 @@
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -162,6 +163,21 @@
method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
}
+ public final class PickVisualMediaRequest {
+ method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+ property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+ }
+
+ public static final class PickVisualMediaRequest.Builder {
+ ctor public PickVisualMediaRequest.Builder();
+ method public androidx.activity.result.PickVisualMediaRequest build();
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
+ public final class PickVisualMediaRequestKt {
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ }
+
}
package androidx.activity.result.contract {
@@ -238,6 +254,49 @@
method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
}
+ @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+ ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+ }
+
+ public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+ ctor public ActivityResultContracts.PickVisualMedia();
+ method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+ method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public static final boolean isPhotoPickerAvailable();
+ method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.Companion {
+ method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+ method public boolean isPhotoPickerAvailable();
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+ method public String getMimeType();
+ property public final String mimeType;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+ }
+
+ public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+ }
+
public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
ctor public ActivityResultContracts.RequestMultiplePermissions();
method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index 3e7fefd..389c8bc 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -115,6 +115,7 @@
finish()
}
}
+ @Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launcher.launch(intent.getParcelableExtra("destinationIntent"))
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
new file mode 100644
index 0000000..5e1790d
--- /dev/null
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2022 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 androidx.activity
+
+import android.os.Build
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S_V2)
+class OnBackPressedDispatcherInvokerTest {
+
+ @Test
+ fun testSimpleInvoker() {
+ var registerCount = 0
+ var unregisterCount = 0
+ val invoker = object : OnBackInvokedDispatcher {
+ override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+ registerCount++
+ }
+
+ override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+ unregisterCount++
+ }
+ }
+
+ val dispatcher = OnBackPressedDispatcher()
+
+ dispatcher.setOnBackInvokedDispatcher(invoker)
+
+ val callback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() { }
+ }
+
+ dispatcher.addCallback(callback)
+
+ assertThat(registerCount).isEqualTo(1)
+
+ callback.remove()
+
+ assertThat(unregisterCount).isEqualTo(1)
+ }
+
+ @Test
+ fun testInvokerEnableDisable() {
+ var registerCount = 0
+ var unregisterCount = 0
+ val invoker = object : OnBackInvokedDispatcher {
+ override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+ registerCount++
+ }
+
+ override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+ unregisterCount++
+ }
+ }
+
+ val dispatcher = OnBackPressedDispatcher()
+
+ dispatcher.setOnBackInvokedDispatcher(invoker)
+
+ val callback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() { }
+ }
+
+ dispatcher.addCallback(callback)
+
+ assertThat(registerCount).isEqualTo(1)
+
+ callback.isEnabled = false
+
+ assertThat(unregisterCount).isEqualTo(1)
+
+ callback.isEnabled = true
+
+ assertThat(registerCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testCallbackEnabledDisabled() {
+ val callback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ TODO("Not yet implemented")
+ }
+ }
+
+ callback.isEnabled = true
+ callback.isEnabled = false
+ }
+
+ @Test
+ fun testInvokerAddDisabledCallback() {
+ var registerCount = 0
+ var unregisterCount = 0
+ val invoker = object : OnBackInvokedDispatcher {
+ override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+ registerCount++
+ }
+
+ override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+ unregisterCount++
+ }
+ }
+
+ val callback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() { }
+ }
+
+ val dispatcher = OnBackPressedDispatcher()
+
+ dispatcher.setOnBackInvokedDispatcher(invoker)
+
+ dispatcher.addCallback(callback)
+
+ assertThat(registerCount).isEqualTo(0)
+
+ callback.isEnabled = true
+
+ assertThat(registerCount).isEqualTo(1)
+
+ callback.isEnabled = false
+
+ assertThat(unregisterCount).isEqualTo(1)
+ }
+
+ @Test
+ fun testInvokerAddEnabledCallbackBeforeSet() {
+ var registerCount = 0
+ var unregisterCount = 0
+ val invoker = object : OnBackInvokedDispatcher {
+ override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+ registerCount++
+ }
+
+ override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+ unregisterCount++
+ }
+ }
+
+ val callback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() { }
+ }
+
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(callback)
+
+ dispatcher.setOnBackInvokedDispatcher(invoker)
+
+ assertThat(registerCount).isEqualTo(1)
+
+ callback.isEnabled = false
+
+ assertThat(unregisterCount).isEqualTo(1)
+ }
+}
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 3f531b3..37422c4 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -65,6 +65,7 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
@@ -76,6 +77,7 @@
import androidx.core.content.ContextCompat;
import androidx.core.content.OnConfigurationChangedProvider;
import androidx.core.content.OnTrimMemoryProvider;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Consumer;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuHostHelper;
@@ -150,8 +152,8 @@
@Override
public void run() {
// Calling onBackPressed() on an Activity with its state saved can cause an
- // error on devices on API levels before 26. We catch that specific error and
- // throw all others.
+ // error on devices on API levels before 26. We catch that specific error
+ // and throw all others.
try {
ComponentActivity.super.onBackPressed();
} catch (IllegalStateException e) {
@@ -170,6 +172,7 @@
private final ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {
+ @SuppressWarnings("deprecation")
@Override
public <I, O> void onLaunch(
final int requestCode,
@@ -344,6 +347,7 @@
* If your ComponentActivity is annotated with {@link ContentView}, this will
* call {@link #setContentView(int)} for you.
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// Restore the Saved State first so that it is available to
@@ -352,6 +356,9 @@
mContextAwareHelper.dispatchOnContextAvailable(this);
super.onCreate(savedInstanceState);
ReportFragment.injectIfNeededIn(this);
+ if (BuildCompat.isAtLeastT()) {
+ mOnBackPressedDispatcher.setOnBackInvokedDispatcher(getOnBackInvokedDispatcher());
+ }
if (mContentLayoutId != 0) {
setContentView(mContentLayoutId);
}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
index 5fc26dc..afa44f3 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
@@ -18,6 +18,10 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
import androidx.lifecycle.LifecycleOwner;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -44,6 +48,7 @@
private boolean mEnabled;
private CopyOnWriteArrayList<Cancellable> mCancellables = new CopyOnWriteArrayList<>();
+ private Consumer<Boolean> mEnabledConsumer;
/**
* Create a {@link OnBackPressedCallback}.
@@ -66,9 +71,13 @@
*
* @param enabled whether the callback should be considered enabled
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@MainThread
public final void setEnabled(boolean enabled) {
mEnabled = enabled;
+ if (mEnabledConsumer != null) {
+ mEnabledConsumer.accept(mEnabled);
+ }
}
/**
@@ -106,4 +115,8 @@
void removeCancellable(@NonNull Cancellable cancellable) {
mCancellables.remove(cancellable);
}
+
+ void setIsEnabledConsumer(@Nullable Consumer<Boolean> isEnabled) {
+ mEnabledConsumer = isEnabled;
+ }
}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
index 6ff39f1..e306a9b 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
@@ -17,10 +17,18 @@
package androidx.activity;
import android.annotation.SuppressLint;
+import android.os.Build;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import androidx.annotation.DoNotInline;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -59,6 +67,42 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
+ private Consumer<Boolean> mEnabledConsumer;
+
+ private OnBackInvokedCallback mOnBackInvokedCallback;
+ private OnBackInvokedDispatcher mInvokedDispatcher;
+ private boolean mBackInvokedCallbackRegistered = false;
+
+ /**
+ * Sets the {@link OnBackInvokedDispatcher} for handling system back for Android SDK T+.
+ *
+ * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public void setOnBackInvokedDispatcher(@NonNull OnBackInvokedDispatcher invoker) {
+ mInvokedDispatcher = invoker;
+ updateBackInvokedCallbackState();
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ void updateBackInvokedCallbackState() {
+ boolean shouldBeRegistered = hasEnabledCallbacks();
+ if (mInvokedDispatcher != null) {
+ if (shouldBeRegistered && !mBackInvokedCallbackRegistered) {
+ Api33Impl.registerOnBackInvokedCallback(
+ mInvokedDispatcher,
+ OnBackInvokedDispatcher.PRIORITY_OVERLAY,
+ mOnBackInvokedCallback
+ );
+ mBackInvokedCallbackRegistered = true;
+ } else if (!shouldBeRegistered && mBackInvokedCallbackRegistered) {
+ Api33Impl.unregisterOnBackInvokedCallback(mInvokedDispatcher,
+ mOnBackInvokedCallback);
+ mBackInvokedCallbackRegistered = false;
+ }
+ }
+ }
+
/**
* Create a new OnBackPressedDispatcher that dispatches System back button pressed events
* to one or more {@link OnBackPressedCallback} instances.
@@ -74,8 +118,22 @@
* @param fallbackOnBackPressed The Runnable that should be triggered if
* {@link #onBackPressed()} is called when {@link #hasEnabledCallbacks()} returns false.
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
mFallbackOnBackPressed = fallbackOnBackPressed;
+ if (BuildCompat.isAtLeastT()) {
+ mEnabledConsumer = aBoolean -> {
+ if (BuildCompat.isAtLeastT()) {
+ updateBackInvokedCallbackState();
+ }
+ };
+ mOnBackInvokedCallback = new OnBackInvokedCallback() {
+ @Override
+ public void onBackInvoked() {
+ onBackPressed();
+ }
+ };
+ }
}
/**
@@ -107,6 +165,7 @@
* @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
* the callback and remove it from the set of OnBackPressedCallbacks.
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@SuppressWarnings("WeakerAccess") /* synthetic access */
@MainThread
@NonNull
@@ -114,6 +173,10 @@
mOnBackPressedCallbacks.add(onBackPressedCallback);
OnBackPressedCancellable cancellable = new OnBackPressedCancellable(onBackPressedCallback);
onBackPressedCallback.addCancellable(cancellable);
+ if (BuildCompat.isAtLeastT()) {
+ updateBackInvokedCallbackState();
+ onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
+ }
return cancellable;
}
@@ -141,6 +204,7 @@
*
* @see #onBackPressed()
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@SuppressLint("LambdaLast")
@MainThread
public void addCallback(@NonNull LifecycleOwner owner,
@@ -152,6 +216,10 @@
onBackPressedCallback.addCancellable(
new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback));
+ if (BuildCompat.isAtLeastT()) {
+ updateBackInvokedCallbackState();
+ onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
+ }
}
/**
@@ -204,10 +272,15 @@
mOnBackPressedCallback = onBackPressedCallback;
}
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@Override
public void cancel() {
mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
mOnBackPressedCallback.removeCancellable(this);
+ if (BuildCompat.isAtLeastT()) {
+ mOnBackPressedCallback.setIsEnabledConsumer(null);
+ updateBackInvokedCallbackState();
+ }
}
}
@@ -251,4 +324,25 @@
}
}
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ static class Api33Impl {
+ private Api33Impl() { }
+
+ @DoNotInline
+ static void registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher onBackInvokedDispatcher, int priority,
+ OnBackInvokedCallback onBackInvokedCallback
+ ) {
+ onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback);
+ }
+
+ @DoNotInline
+ static void unregisterOnBackInvokedCallback(
+ OnBackInvokedDispatcher onBackInvokedDispatcher,
+ OnBackInvokedCallback onBackInvokedCallback
+ ) {
+ onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback);
+ }
+ }
}
diff --git a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
new file mode 100644
index 0000000..f9b3bee
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 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 androidx.activity.result
+
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
+
+/**
+ * Creates a request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ *
+ * @param mediaType type to go into the PickVisualMediaRequest
+ *
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+fun PickVisualMediaRequest(
+ mediaType: VisualMediaType = ImageAndVideo
+) = PickVisualMediaRequest.Builder().setMediaType(mediaType).build()
+
+/**
+ * A request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ */
+class PickVisualMediaRequest internal constructor() {
+
+ var mediaType: VisualMediaType = ImageAndVideo
+ private set
+
+ /**
+ * A builder for constructing [PickVisualMediaRequest] instances.
+ */
+ class Builder {
+
+ private var mediaType: VisualMediaType = ImageAndVideo
+
+ /**
+ * Set the media type for the [PickVisualMediaRequest].
+ *
+ * The type is the mime type to filter by, e.g. `PickVisualMedia.ImageOnly`,
+ * `PickVisualMedia.ImageAndVideo`, `PickVisualMedia.SingleMimeType("image/gif")`
+ *
+ * @param mediaType type to go into the PickVisualMediaRequest
+ * @return This builder.
+ */
+ fun setMediaType(mediaType: VisualMediaType): Builder {
+ this.mediaType = mediaType
+ return this
+ }
+
+ /**
+ * Build the PickVisualMediaRequest specified by this builder.
+ *
+ * @return the newly constructed PickVisualMediaRequest.
+ */
+ fun build(): PickVisualMediaRequest = PickVisualMediaRequest(mediaType).apply {
+ this.mediaType = mediaType
+ }
+ }
+}
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index 9af89ed..aba00e1 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -22,11 +22,13 @@
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
+import android.os.ext.SdkExtensions.getExtensionVersion
import android.provider.ContactsContract
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.Companion.getClipDataUris
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion.ACTION_INTENT_SENDER_REQUEST
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion.EXTRA_SEND_INTENT_EXCEPTION
@@ -257,6 +259,7 @@
input: Void?
): SynchronousResult<Bitmap?>? = null
+ @Suppress("DEPRECATION")
final override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.getParcelableExtra("data")
}
@@ -317,6 +320,7 @@
input: Uri
): SynchronousResult<Bitmap?>? = null
+ @Suppress("DEPRECATION")
final override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.getParcelableExtra("data")
}
@@ -597,4 +601,155 @@
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
+
+ /**
+ * An [ActivityResultContract] to use the photo picker through [MediaStore.ACTION_PICK_IMAGES]
+ * when available, and else rely on ACTION_OPEN_DOCUMENT.
+ *
+ * The input is a [PickVisualMediaRequest].
+ *
+ * The output is a `Uri` when the user has selected a media or `null` when the user hasn't
+ * selected any item. Keep in mind that `Uri` returned by the photo picker isn't writable.
+ *
+ * This can be extended to override [createIntent] if you wish to pass additional
+ * extras to the Intent created by `super.createIntent()`.
+ */
+ open class PickVisualMedia : ActivityResultContract<PickVisualMediaRequest, Uri?>() {
+ companion object {
+ @JvmStatic
+ fun isPhotoPickerAvailable(): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ||
+ getExtensionVersion(Build.VERSION_CODES.R) >= 2
+ }
+
+ @JvmStatic
+ fun getVisualMimeType(input: VisualMediaType): String? {
+ return when (input) {
+ is ImageOnly -> "image/*"
+ is VideoOnly -> "video/*"
+ is SingleMimeType -> input.mimeType
+ is ImageAndVideo -> null
+ }
+ }
+ }
+
+ sealed interface VisualMediaType
+ object ImageOnly : VisualMediaType
+ object VideoOnly : VisualMediaType
+ object ImageAndVideo : VisualMediaType
+ class SingleMimeType(val mimeType: String) : VisualMediaType
+
+ @CallSuper
+ override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
+ // Check if Photo Picker is available on the device
+ return if (isPhotoPickerAvailable()) {
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ type = getVisualMimeType(input.mediaType)
+ }
+ } else {
+ // For older devices running KitKat and higher and devices running Android 12
+ // and 13 without the SDK extension that includes the Photo Picker, rely on the
+ // ACTION_OPEN_DOCUMENT intent
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = getVisualMimeType(input.mediaType)
+
+ if (type == null) {
+ // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+ // intent with multiple mime types
+ type = "*/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+ }
+ }
+ }
+ }
+
+ @Suppress("InvalidNullability")
+ final override fun getSynchronousResult(
+ context: Context,
+ input: PickVisualMediaRequest
+ ): SynchronousResult<Uri?>? = null
+
+ final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
+ return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
+ }
+ }
+
+ /**
+ * An [ActivityResultContract] to use the Photo Picker through [MediaStore.ACTION_PICK_IMAGES]
+ * when available, and else rely on ACTION_OPEN_DOCUMENT.
+ *
+ * The constructor accepts one parameter `maxItems` to limit the number of selectable items when
+ * using the photo picker to return. Keep in mind that this parameter isn't supported on devices
+ * when the photo picker isn't available.
+ *
+ * The input is a [PickVisualMediaRequest].
+ *
+ * The output is a list `Uri` of the selected media. It can be empty if the user hasn't selected
+ * any items. Keep in mind that `Uri` returned by the photo picker aren't writable.
+ *
+ * This can be extended to override [createIntent] if you wish to pass additional
+ * extras to the Intent created by `super.createIntent()`.
+ */
+ @RequiresApi(19)
+ open class PickMultipleVisualMedia(
+ private val maxItems: Int = getMaxItems()
+ ) : ActivityResultContract<PickVisualMediaRequest, List<@JvmSuppressWildcards Uri>>() {
+
+ init {
+ require(maxItems > 1) {
+ "Max items must be higher than 1"
+ }
+ }
+
+ @CallSuper
+ override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
+ // Check to see if the photo picker is available
+ return if (PickVisualMedia.isPhotoPickerAvailable()) {
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ type = PickVisualMedia.getVisualMimeType(input.mediaType)
+
+ require(maxItems <= MediaStore.getPickImagesMaxLimit()) {
+ "Max items must be less or equals MediaStore.getPickImagesMaxLimit()"
+ }
+
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxItems)
+ }
+ } else {
+ // For older devices running KitKat and higher and devices running Android 12
+ // and 13 without the SDK extension that includes the Photo Picker, rely on the
+ // ACTION_OPEN_DOCUMENT intent
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = PickVisualMedia.getVisualMimeType(input.mediaType)
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+
+ if (type == null) {
+ // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+ // intent with multiple mime types
+ type = "*/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+ }
+ }
+ }
+ }
+
+ @Suppress("InvalidNullability")
+ final override fun getSynchronousResult(
+ context: Context,
+ input: PickVisualMediaRequest
+ ): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
+
+ final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
+ return intent.takeIf {
+ resultCode == Activity.RESULT_OK
+ }?.getClipDataUris() ?: emptyList()
+ }
+
+ internal companion object {
+ internal fun getMaxItems() = if (PickVisualMedia.isPhotoPickerAvailable()) {
+ MediaStore.getPickImagesMaxLimit()
+ } else {
+ Integer.MAX_VALUE
+ }
+ }
+ }
}
diff --git a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
index 11780b3..947be53 100644
--- a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
+++ b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
@@ -36,11 +36,14 @@
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CaptureVideo
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
+import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.TakePicture
import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview
@@ -77,6 +80,10 @@
toast("Got image: $uri")
}
+ lateinit var pickVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
+
+ lateinit var pickMultipleVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
+
lateinit var createDocument: ActivityResultLauncher<String>
lateinit var openDocuments: ActivityResultLauncher<Array<String>>
@@ -92,6 +99,17 @@
super.onCreate(savedInstanceState)
if (android.os.Build.VERSION.SDK_INT >= 19) {
+ pickVisualMedia = registerForActivityResult(PickVisualMedia()) { uri ->
+ toast("Got image: $uri")
+ }
+ pickMultipleVisualMedia =
+ registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
+ var media = ""
+ uris.forEach {
+ media += "uri: $it \n"
+ }
+ toast("Got media files: $media")
+ }
createDocument = registerForActivityResult(CreateDocument("image/png")) { uri ->
toast("Created document: $uri")
}
@@ -124,10 +142,25 @@
val uri = FileProvider.getUriForFile(this@MainActivity, packageName, file)
captureVideo.launch(uri)
}
- button("Pick an image") {
+ button("Pick an image (w/ GET_CONTENT)") {
getContent.launch("image/*")
}
if (android.os.Build.VERSION.SDK_INT >= 19) {
+ button("Pick an image (w/ photo picker)") {
+ pickVisualMedia.launch(
+ PickVisualMediaRequest(PickVisualMedia.ImageOnly)
+ )
+ }
+ button("Pick a GIF (w/ photo picker)") {
+ pickVisualMedia.launch(
+ PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif"))
+ )
+ }
+ button("Pick 5 visual media max (w/ photo picker)") {
+ pickMultipleVisualMedia.launch(
+ PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
+ )
+ }
button("Create document") {
createDocument.launch("Temp")
}
diff --git a/appcompat/appcompat/api/current.txt b/appcompat/appcompat/api/current.txt
index 6acef29..3fa4361 100644
--- a/appcompat/appcompat/api/current.txt
+++ b/appcompat/appcompat/api/current.txt
@@ -224,6 +224,7 @@
method public androidx.appcompat.app.ActionBar? getSupportActionBar();
method public android.content.Intent? getSupportParentActivityIntent();
method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+ method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
method public final boolean onMenuItemSelected(int, android.view.MenuItem);
method protected void onNightModeChanged(int);
method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -261,6 +262,8 @@
method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+ method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+ method public android.content.Context? getContextForDelegate();
method public static int getDefaultNightMode();
method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
method public int getLocalNightMode();
@@ -280,6 +283,7 @@
method public abstract void onStart();
method public abstract void onStop();
method public abstract boolean requestWindowFeature(int);
+ method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
method public static void setCompatVectorFromResourcesEnabled(boolean);
method public abstract void setContentView(android.view.View!);
method public abstract void setContentView(@LayoutRes int);
@@ -339,6 +343,12 @@
method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
}
+ public final class AppLocalesMetadataHolderService extends android.app.Service {
+ ctor public AppLocalesMetadataHolderService();
+ method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.os.IBinder onBind(android.content.Intent);
+ }
+
}
package androidx.appcompat.graphics.drawable {
diff --git a/appcompat/appcompat/api/public_plus_experimental_current.txt b/appcompat/appcompat/api/public_plus_experimental_current.txt
index 6acef29..3fa4361 100644
--- a/appcompat/appcompat/api/public_plus_experimental_current.txt
+++ b/appcompat/appcompat/api/public_plus_experimental_current.txt
@@ -224,6 +224,7 @@
method public androidx.appcompat.app.ActionBar? getSupportActionBar();
method public android.content.Intent? getSupportParentActivityIntent();
method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+ method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
method public final boolean onMenuItemSelected(int, android.view.MenuItem);
method protected void onNightModeChanged(int);
method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -261,6 +262,8 @@
method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+ method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+ method public android.content.Context? getContextForDelegate();
method public static int getDefaultNightMode();
method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
method public int getLocalNightMode();
@@ -280,6 +283,7 @@
method public abstract void onStart();
method public abstract void onStop();
method public abstract boolean requestWindowFeature(int);
+ method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
method public static void setCompatVectorFromResourcesEnabled(boolean);
method public abstract void setContentView(android.view.View!);
method public abstract void setContentView(@LayoutRes int);
@@ -339,6 +343,12 @@
method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
}
+ public final class AppLocalesMetadataHolderService extends android.app.Service {
+ ctor public AppLocalesMetadataHolderService();
+ method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.os.IBinder onBind(android.content.Intent);
+ }
+
}
package androidx.appcompat.graphics.drawable {
diff --git a/appcompat/appcompat/api/restricted_current.txt b/appcompat/appcompat/api/restricted_current.txt
index 6c1a9f8..9a4e193 100644
--- a/appcompat/appcompat/api/restricted_current.txt
+++ b/appcompat/appcompat/api/restricted_current.txt
@@ -245,6 +245,7 @@
method public androidx.appcompat.app.ActionBar? getSupportActionBar();
method public android.content.Intent? getSupportParentActivityIntent();
method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+ method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
method public final boolean onMenuItemSelected(int, android.view.MenuItem);
method protected void onNightModeChanged(@androidx.appcompat.app.AppCompatDelegate.NightMode int);
method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -282,6 +283,8 @@
method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+ method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+ method public android.content.Context? getContextForDelegate();
method @androidx.appcompat.app.AppCompatDelegate.NightMode public static int getDefaultNightMode();
method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
method @androidx.appcompat.app.AppCompatDelegate.NightMode public int getLocalNightMode();
@@ -301,6 +304,7 @@
method public abstract void onStart();
method public abstract void onStop();
method public abstract boolean requestWindowFeature(int);
+ method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
method public static void setCompatVectorFromResourcesEnabled(boolean);
method public abstract void setContentView(android.view.View!);
method public abstract void setContentView(@LayoutRes int);
@@ -363,6 +367,12 @@
method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
}
+ public final class AppLocalesMetadataHolderService extends android.app.Service {
+ ctor public AppLocalesMetadataHolderService();
+ method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.os.IBinder onBind(android.content.Intent);
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class WindowDecorActionBar extends androidx.appcompat.app.ActionBar implements androidx.appcompat.widget.ActionBarOverlayLayout.ActionBarVisibilityCallback {
ctor public WindowDecorActionBar(android.app.Activity!, boolean);
ctor public WindowDecorActionBar(android.app.Dialog!);
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index b5a068e..cede9fd 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -26,6 +26,15 @@
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
+ <service
+ android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+ android:enabled="false"
+ android:exported="false">
+ <meta-data
+ android:name="autoStoreLocales"
+ android:value="false" />
+ </service>
+
<activity
android:name="androidx.appcompat.app.AppCompatActivity"/>
@@ -324,6 +333,40 @@
<activity
android:name="androidx.appcompat.widget.TooltipCompatTestActivity" />
+ <activity android:name="androidx.appcompat.app.LocalesUpdateActivity"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesConfigChangesActivity"
+ android:configChanges="locale|layoutDirection"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesCustomApplyOverrideConfigurationActivity" />
+
+ <activity android:name="androidx.appcompat.app.LocalesDialogFragment"/>
+
+ <activity android:name="androidx.appcompat.app.LocalesCustomAttachBaseContextActivity"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesRotateDoesNotRecreateActivity"
+ android:configChanges="orientation|screenSize"/>
+
+ <activity android:name="androidx.appcompat.app.LocalesActivityA"/>
+
+ <activity android:name="androidx.appcompat.app.LocalesActivityB"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesConfigChangesActivityA"
+ android:configChanges="locale|layoutDirection"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesConfigChangesActivityB"
+ android:configChanges="locale|layoutDirection"/>
+
+ <activity
+ android:name="androidx.appcompat.app.LocalesConfigChangesActivityWithoutLayoutDirection"
+ android:configChanges="locale"/>
+
+ <activity android:name="androidx.appcompat.app.LocalesLateOnCreateActivity"/>
</application>
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
new file mode 100644
index 0000000..5719451
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+/**
+ * An activity for locales with a unique class name.
+ */
+public class LocalesActivityA extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java
new file mode 100644
index 0000000..b327efb
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+/**
+ * An activity for locales with a unique class name.
+ */
+public class LocalesActivityB extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt
new file mode 100644
index 0000000..9ae3adda
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import android.content.Intent
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.TestCase.assertNotSame
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesChangeWhenInBackground {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setUp() {
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(
+ rule.activity.resources.configuration)
+ }
+
+ @Test
+ fun testLocalesChangeWhenInBackground() {
+
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val firstActivity = rule.activity
+ assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+ // Start a new Activity, so that the original Activity goes into the background
+ val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+ assertConfigurationLocalesEquals(systemLocales, secondActivity)
+
+ // Now change the locales for the foreground activity
+ val recreatedSecond = setLocalesAndWaitForRecreate(
+ secondActivity,
+ CUSTOM_LOCALE_LIST
+ )
+
+ // Now finish the foreground activity and wait until it is destroyed,
+ // allowing the recreated activity to come to the foreground
+ instrumentation.runOnMainSync { recreatedSecond.finish() }
+ waitUntilState(recreatedSecond, Lifecycle.State.DESTROYED)
+
+ // Assert that the recreated Activity becomes resumed
+ instrumentation.waitForIdleSync()
+ assertNotSame(rule.activity, firstActivity)
+ waitUntilState(rule.activity, Lifecycle.State.RESUMED)
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java
new file mode 100644
index 0000000..d7c757e
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivity extends LocalesUpdateActivity { }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java
new file mode 100644
index 0000000..c6b0d8b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivityA extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java
new file mode 100644
index 0000000..c59c33b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivityB extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java
new file mode 100644
index 0000000..77d7bfb
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 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 androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes and does not handle layoutDirection
+ * configChanges.
+ */
+public class LocalesConfigChangesActivityWithoutLayoutDirection extends LocalesUpdateActivity { }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt
new file mode 100644
index 0000000..a4aff79
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocales
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.withActivity
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesConfigChangesTestCase() {
+ private lateinit var scenario: ActivityScenario<LocalesConfigChangesActivity>
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setup() {
+ LocalesUtils.initCustomLocaleList()
+ // By default we'll set the apps to use system locales, which allows us to make better
+ // assumptions in the tests below.
+ // Launch the test activity.
+ scenario = ActivityScenario.launch(LocalesConfigChangesActivity::class.java)
+ scenario.onActivity {
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(it.resources.configuration)
+ // expected locales is an overlay of custom and system locales.
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales)
+ }
+ }
+
+ @Test
+ fun testOnConfigurationChangeCalledWhileStarted() {
+ scenario.moveToState(Lifecycle.State.RESUMED)
+
+ // Set locales to CUSTOM_LOCALE_LIST.
+ scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+ // Assert that the onConfigurationChange was called with a new correct config.
+ scenario.onActivity {
+ val lastConfig = it.lastConfigurationChangeAndClear
+ assertConfigurationLocalesEquals(
+ expectedLocales,
+ lastConfig!!
+ )
+ }
+
+ // Set locales back to system locales.
+ scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+ // Assert that the onConfigurationChange was called with a new correct config.
+ scenario.onActivity {
+ val lastConfig = it.lastConfigurationChangeAndClear
+ assertConfigurationLocalesEquals(
+ systemLocales, lastConfig!!
+ )
+ }
+ }
+
+ @Test
+ fun testOnConfigurationChangeCalledWhileStopped() {
+ scenario.moveToState(Lifecycle.State.RESUMED)
+ scenario.moveToState(Lifecycle.State.CREATED)
+
+ // Set locales to CUSTOM_LOCALE_LIST.
+ scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+ // Assert that the onConfigurationChange was called with a new correct config.
+ scenario.onActivity {
+ val lastConfig = it.lastConfigurationChangeAndClear
+ assertConfigurationLocalesEquals(
+ expectedLocales,
+ lastConfig!!
+ )
+ }
+
+ // Set locales back to system locales.
+ scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+ // Assert that the onConfigurationChange was called with a new correct config.
+ scenario.onActivity {
+ val lastConfig = it.lastConfigurationChangeAndClear
+ assertConfigurationLocalesEquals(
+ systemLocales, lastConfig!!
+ )
+ }
+ }
+
+ @Test
+ fun testOnConfigurationChangeNotCalledWhileDestroyed() {
+ scenario.moveToState(Lifecycle.State.RESUMED)
+
+ lateinit var activity: LocalesConfigChangesActivity
+ scenario.onActivity { activity = it }
+
+ scenario.moveToState(Lifecycle.State.DESTROYED)
+
+ // Clear any previous config changes.
+ activity.lastConfigurationChangeAndClear
+
+ // Set locales to CUSTOM_LOCALE_LIST.
+ setLocales(CUSTOM_LOCALE_LIST)
+ // Assert that the onConfigurationChange was not called with a new correct config.
+ assertNull(activity.lastConfigurationChangeAndClear)
+
+ // Set locales back to system locales.
+ setLocales(LocaleListCompat.getEmptyLocaleList())
+ // Assert that the onConfigurationChange was not called with a new correct config.
+ assertNull(activity.lastConfigurationChangeAndClear)
+ }
+
+ @Test
+ fun testResourcesUpdated() {
+ // Set locales to CUSTOM_LOCALE_LIST.
+ scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+
+ // Assert that the Activity resources configuration was updated.
+ assertConfigurationLocalesEquals(
+ expectedLocales,
+ scenario.withActivity { this }
+ )
+
+ // Set locales back to system locales.
+ scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+
+ // Assert that the Activity resources configuration was updated.
+ assertConfigurationLocalesEquals(
+ systemLocales,
+ scenario.withActivity { this }
+ )
+ }
+
+ @Test
+ fun testOnLocalesChangedCalled() {
+ // Set locales to CUSTOM_LOCALE_LIST.
+ scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+ // Assert that the Activity received a new value.
+ assertEquals(
+ expectedLocales,
+ scenario.withActivity { lastLocalesAndReset }
+ )
+
+ // Set locales back to system locales.
+ scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+ // Assert that the Activity received a new value.
+ assertEquals(systemLocales, scenario.withActivity { lastLocalesAndReset })
+ }
+
+ @After
+ fun cleanup() {
+ // Reset the system locales.
+ if (scenario.state != Lifecycle.State.DESTROYED) {
+ scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+ }
+ LocalesUpdateActivity.teardown()
+ scenario.close()
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt
new file mode 100644
index 0000000..c20886a
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.filters.SdkSuppress
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesConfigChangesWithoutLayoutDirectionTestCase {
+ private lateinit var scenario: ActivityScenario<
+ LocalesConfigChangesActivityWithoutLayoutDirection>
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setup() {
+ LocalesUtils.initCustomLocaleList()
+ // By default we'll set the apps to use system locales, which allows us to make better
+ // assumptions in the tests below.
+ // Launch the test activity.
+ scenario =
+ ActivityScenario.launch(LocalesConfigChangesActivityWithoutLayoutDirection::class.java)
+ scenario.onActivity {
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(it.resources.configuration)
+ // expected locales is an overlay of custom and system locales.
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ LocalesUtils.CUSTOM_LOCALE_LIST, systemLocales
+ )
+ }
+ }
+
+ @Test
+ fun testOnConfigurationChangeNotCalledWhileStarted() {
+ scenario.moveToState(Lifecycle.State.RESUMED)
+
+ // Set locales to CUSTOM_LOCALE_LIST.
+ scenario.onActivity { LocalesUtils.setLocales(LocalesUtils.CUSTOM_LOCALE_LIST) }
+ // Assert that the onConfigurationChange was called with a new correct config.
+ scenario.onActivity {
+ // the call should not have reached the LocalesUpdateActivity.onConfigurationChange()
+ // because the manifest entry for LocalesConfigChangesActivityWithoutLayoutDirection
+ // only handles locale and not layoutDir.
+ assertNull(it.lastConfigurationChangeAndClear)
+ LocalesUtils.assertConfigurationLocalesEquals(
+ expectedLocales,
+ it.resources.configuration!!
+ )
+ }
+ }
+
+ @After
+ fun teardown() {
+ LocalesUpdateActivity.teardown()
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java
new file mode 100644
index 0000000..e39c081
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * An activity that has a customized fontScale, set before onCreate().
+ */
+@RequiresApi(17)
+public class LocalesCustomApplyOverrideConfigurationActivity extends LocalesUpdateActivity {
+ public static final float CUSTOM_FONT_SCALE = 4.24f;
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(newBase);
+
+ Configuration config = new Configuration();
+ config.fontScale = CUSTOM_FONT_SCALE;
+ super.applyOverrideConfiguration(config);
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt
new file mode 100644
index 0000000..27c3d16
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import android.content.res.Configuration
+import androidx.appcompat.app.LocalesCustomApplyOverrideConfigurationActivity.CUSTOM_FONT_SCALE
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.testutils.NightModeUtils
+import androidx.appcompat.testutils.NightModeUtils.assertConfigurationNightModeEquals
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * This is one approach to customize Activity's configuration that's used in google3.
+ */
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 17, maxSdkVersion = 31)
+class LocalesCustomApplyOverrideConfigurationTestCase() {
+
+ @get:Rule
+ val activityRule = LocalesActivityTestRule(
+ LocalesCustomApplyOverrideConfigurationActivity::class.java
+ )
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun testNightModeIsMaintainedOnLocalesChange() {
+ NightModeUtils.setNightModeAndWaitForRecreate(
+ activityRule,
+ AppCompatDelegate.MODE_NIGHT_YES,
+ NightModeUtils.NightSetMode.LOCAL
+ )
+ assertConfigurationNightModeEquals(
+ Configuration.UI_MODE_NIGHT_YES,
+ activityRule.activity.resources.configuration
+ )
+ setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+ // Check that the custom configuration properties are maintained
+ assertConfigurationNightModeEquals(
+ Configuration.UI_MODE_NIGHT_YES,
+ activityRule.activity.resources.configuration
+ )
+ setLocalesAndWaitForRecreate(activityRule, LocaleListCompat.getEmptyLocaleList())
+ }
+
+ @Test
+ fun testFontScaleIsMaintained() {
+ // Check that the custom configuration properties are maintained
+ val config = activityRule.activity.resources.configuration
+ assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+ }
+
+ @Test
+ fun testFontScaleIsMaintainedOnLocalesChange() {
+ // Set locales to CUSTOM_LOCALE_LIST.
+ setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+ // Check that the custom configuration properties are maintained.
+ val config = activityRule.activity.resources.configuration
+ assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.java
new file mode 100644
index 0000000..9f929e5
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+
+/**
+ * An activity with customized configuration.
+ */
+public class LocalesCustomAttachBaseContextActivity extends LocalesUpdateActivity {
+ public static final float CUSTOM_FONT_SCALE = 4.24f;
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(useCustomConfig(newBase));
+ }
+
+ private Context useCustomConfig(Context context) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Configuration config = new Configuration();
+ config.fontScale = CUSTOM_FONT_SCALE;
+ return context.createConfigurationContext(config);
+ } else {
+ Resources res = context.getResources();
+ Configuration config = new Configuration(res.getConfiguration());
+ config.fontScale = CUSTOM_FONT_SCALE;
+ res.updateConfiguration(config, res.getDisplayMetrics());
+ return context;
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt
new file mode 100644
index 0000000..9215bf4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import android.content.res.Configuration
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
+import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_FONT_SCALE
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.testutils.NightModeUtils.NightSetMode
+import androidx.appcompat.testutils.NightModeUtils.assertConfigurationNightModeEquals
+import androidx.appcompat.testutils.NightModeUtils.setNightModeAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesCustomAttachBaseContextTestCase() {
+
+ @get:Rule
+ val activityRule = LocalesActivityTestRule(
+ LocalesCustomAttachBaseContextActivity::class.java
+ )
+
+ @Before
+ fun setUp() {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun testNightModeIsMaintainedOnLocalesChange() {
+ setNightModeAndWaitForRecreate(
+ activityRule,
+ MODE_NIGHT_YES,
+ NightSetMode.LOCAL
+ )
+ assertConfigurationNightModeEquals(
+ Configuration.UI_MODE_NIGHT_YES,
+ activityRule.activity.resources.configuration
+ )
+ setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+ // Check that the custom configuration properties are maintained
+ assertConfigurationNightModeEquals(
+ Configuration.UI_MODE_NIGHT_YES,
+ activityRule.activity.resources.configuration
+ )
+ }
+
+ @Test
+ fun testFontScaleIsMaintained() {
+ // Check that the custom configuration properties are maintained.
+ val config = activityRule.activity.resources.configuration
+ assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+ }
+
+ @Test
+ fun testFontScaleIsMaintainedOnLocalesChange() {
+ // Set locales to CUSTOM_LOCALE_LIST.
+ setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+ // Check that the custom configuration properties are maintained.
+ val config = activityRule.activity.resources.configuration
+ assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt
new file mode 100644
index 0000000..7ced0ff
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.TestCase.assertNotSame
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesForegroundDialogTestCase {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ private var baseLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setUp() {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ baseLocales = LocalesUpdateActivity.getConfigLocales(rule.activity.resources.configuration)
+ }
+
+ @Test
+ fun testLocalesChangeWithForegroundDialog() {
+ val firstActivity = rule.activity
+ assertConfigurationLocalesEquals(baseLocales, firstActivity)
+
+ // Open a dialog on top of the activity.
+ rule.runOnUiThread {
+ val frag = TestDialogFragment.newInstance()
+ frag.show(firstActivity.supportFragmentManager, "dialog")
+ }
+
+ // Now change the locales for the foreground activity.
+ setLocalesAndWaitForRecreate(
+ firstActivity,
+ CUSTOM_LOCALE_LIST
+ )
+
+ // Ensure that it was recreated.
+ assertNotSame(rule.activity, firstActivity)
+ waitUntilState(rule.activity, Lifecycle.State.RESUMED)
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
new file mode 100644
index 0000000..6466e41
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.LocaleList;
+
+import androidx.core.os.LocaleListCompat;
+
+import java.util.Locale;
+
+/**
+ * An activity with systemLocales replaced with some customised locales for testing.
+ */
+public class LocalesLateOnCreateActivity extends LocalesUpdateActivity {
+
+ public static LocaleListCompat DEFAULT_LOCALE_LIST;
+ public static LocaleListCompat TEST_LOCALE_LIST;
+ public static LocaleListCompat EXPECTED_LOCALE_LIST;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ // Override locales so that AppCompat attempts to re-apply during onCreate().
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ DEFAULT_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+ Locale.US.toLanguageTag() + "," + Locale.CHINESE.toLanguageTag());
+ TEST_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+ Locale.CANADA_FRENCH.toLanguageTag() + ","
+ + Locale.US.toLanguageTag());
+ EXPECTED_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+ Locale.CANADA_FRENCH.toLanguageTag() + ","
+ + Locale.US.toLanguageTag() + "," + Locale.CHINESE.toLanguageTag());
+ } else {
+ DEFAULT_LOCALE_LIST = LocaleListCompat.create(Locale.US);
+ TEST_LOCALE_LIST = LocaleListCompat.create(Locale.CANADA_FRENCH);
+ EXPECTED_LOCALE_LIST = LocaleListCompat.create(Locale.CANADA_FRENCH);
+ }
+ disableAutomaticLocales(getApplicationContext());
+
+ super.onCreate(bundle);
+ }
+
+ private static void setLocales(LocaleListCompat locales, Context context) {
+ Configuration conf = context.getResources().getConfiguration();
+ if (Build.VERSION.SDK_INT >= 24) {
+ conf.setLocales(LocaleList.forLanguageTags(locales.toLanguageTags()));
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ conf.setLocale(locales.get(0));
+ } else {
+ conf.locale = locales.get(0);
+ }
+ // updateConfiguration is required to make the configuration change stick.
+ // updateConfiguration must be called before any use of the actual Resources.
+ context.getResources().updateConfiguration(conf,
+ context.getResources().getDisplayMetrics());
+ }
+
+ /**
+ * Ensures the context does not use system locales, instead uses the DEFAULT_LOCALE_LIST
+ *
+ * <p>This must be called before a Context's Resources are used for the first time. {@code
+ * Activity.onCreate} is a great place to call {@code disableAutomaticLocales(this)}
+ */
+ public static void disableAutomaticLocales(Context context) {
+ setLocales(DEFAULT_LOCALE_LIST, context);
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt
new file mode 100644
index 0000000..13968ee
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import androidx.appcompat.app.LocalesLateOnCreateActivity.DEFAULT_LOCALE_LIST
+import androidx.appcompat.app.LocalesLateOnCreateActivity.EXPECTED_LOCALE_LIST
+import androidx.appcompat.app.LocalesLateOnCreateActivity.TEST_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesLateOnCreateTestCase {
+
+ @get:Rule
+ val activityRule = LocalesActivityTestRule(LocalesLateOnCreateActivity::class.java)
+
+ @Test
+ fun testActivityRecreateLoop() {
+ // Activity should be able to reach fully resumed state in default locales.
+ waitUntilState(activityRule.activity, Lifecycle.State.RESUMED)
+ assertConfigurationLocalesEquals(
+ DEFAULT_LOCALE_LIST,
+ activityRule.activity.resources.configuration
+ )
+
+ // Simulate the user set locales, which should force an activity recreate().
+ setLocalesAndWaitForRecreate(
+ activityRule,
+ TEST_LOCALE_LIST
+ )
+
+ // Activity should be able to reach fully resumed state again.
+ waitUntilState(activityRule.activity, Lifecycle.State.RESUMED)
+
+ // The requested locales should have been set during attachBaseContext().
+ assertConfigurationLocalesEquals(
+ EXPECTED_LOCALE_LIST,
+ activityRule.activity.resources.configuration
+ )
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
new file mode 100644
index 0000000..0470ce5f
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2022 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.content.Intent
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesPersistTestCase() {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setUp() {
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(
+ rule.activity.resources.configuration
+ )
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales
+ )
+ }
+
+ /**
+ * This test verifies that the locales persist in storage when a metadata entry for
+ * "autoStoreLocale" is provided as an opt-in.
+ * To replicate the scenario of app-startup a method resetStaticRequestedAndStoredLocales()
+ * is called to clear out the static storage of locales. The flow of the test is:
+ * setApplicationLocales is called on the firstActivity and it is recreated as recreatedFirst.
+ * Now the locales must have been stored and the static storage would also hold these. Then
+ * we clear out the static storage and create a new activity secondActivity by using an intent
+ * and because the static storage was already clear the activity should sync locales from
+ * storage and start up in the app-specific locales.
+ */
+ @Test
+ fun testLocalesAppliedInNewActivityAfterStaticStorageCleared() {
+ // mimics opting in to "autoStoreLocales"
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val firstActivity = rule.activity
+ assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+ // Now change the locales for the activity
+ val recreatedFirst = setLocalesAndWaitForRecreate(
+ firstActivity,
+ CUSTOM_LOCALE_LIST
+ )
+
+ assertConfigurationLocalesEquals(expectedLocales, recreatedFirst)
+
+ // clear out static storage so that, when a new activity starts it syncs locales
+ // from storage.
+ AppCompatDelegate.resetStaticRequestedAndStoredLocales()
+ // verify that the static locales were cleared out.
+ assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+ // Start a new Activity, so that the original Activity goes into the background
+ val intent = Intent(recreatedFirst, AppCompatActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+ // assert that the new activity started with the app-specific locales after reading them
+ // from storage.
+ assertConfigurationLocalesEquals(expectedLocales, secondActivity)
+ }
+
+ @Test
+ fun testNewActivityCreatedWhenNoAppLocalesExist() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val firstActivity = rule.activity
+ assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+ // Start a new Activity, so that the original Activity goes into the background
+ val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+ // assert that the new activity started with the systemLocales.
+ assertConfigurationLocalesEquals(systemLocales, secondActivity)
+ }
+
+ @After
+ fun teardown() {
+ rule.runOnUiThread {
+ // clean-up
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ // setting auto storage opt-in to false
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java
new file mode 100644
index 0000000..14efcde
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+public class LocalesRotateDoesNotRecreateActivity extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt
new file mode 100644
index 0000000..ef28708
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import androidx.appcompat.Orientation
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.withOrientation
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.LifecycleOwnerUtils
+import org.junit.After
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 18, maxSdkVersion = 31)
+class LocalesRotateDoesNotRecreateActivityTestCase() {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @get:Rule
+ val activityRule: LocalesActivityTestRule<LocalesRotateDoesNotRecreateActivity> =
+ LocalesActivityTestRule(
+ LocalesRotateDoesNotRecreateActivity::class.java,
+ initialTouchMode = false,
+ // Let the test method launch its own activity so that we can ensure it's RESUMED.
+ launchActivity = false
+ )
+
+ @Test
+ fun testRotateDoesNotRecreateActivity() {
+ // Set locales to CUSTOM_LOCALE_LIST and wait for state RESUMED.
+ val initialActivity = activityRule.launchActivity(null)
+ var systemLocales = LocalesUpdateActivity.getConfigLocales(
+ initialActivity.resources.configuration)
+ LifecycleOwnerUtils.waitUntilState(initialActivity, Lifecycle.State.RESUMED)
+ setLocalesAndWaitForRecreate(initialActivity, CUSTOM_LOCALE_LIST)
+
+ val localesActivity = activityRule.activity
+ val config = localesActivity.resources.configuration
+
+ // On API level 26 and below, the configuration object is going to be identical
+ // across configuration changes, so we need to pull the orientation value now.
+ val orientation = config.orientation
+ val expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales)
+ // Assert that the current Activity has the new locales.
+ assertConfigurationLocalesEquals(expectedLocales, config)
+
+ // Now rotate the device. This should NOT result in a lifecycle event, just a call to
+ // onConfigurationChanged.
+ localesActivity.resetOnConfigurationChange()
+ device.withOrientation(Orientation.LEFT) {
+ instrumentation.waitForIdleSync()
+ localesActivity.expectOnConfigurationChange(5000)
+
+ // Assert that we got the same activity and thus it was not recreated.
+ val rotatedLocalesActivity = activityRule.activity
+ val rotatedConfig = rotatedLocalesActivity.resources.configuration
+ assertSame(localesActivity, rotatedLocalesActivity)
+ assertConfigurationLocalesEquals(expectedLocales, rotatedConfig)
+
+ // On API level 26 and below, the configuration object is going to be identical
+ // across configuration changes, so we need to compare against the cached value.
+ assertNotSame(orientation, rotatedConfig.orientation)
+ }
+ }
+
+ @After
+ fun teardown() {
+ device.setOrientationNatural()
+ // setOrientationNatural may need some time rotate orientation to natural, so we wait for
+ // the operation to end for 5000ms.
+ device.waitForIdle(/* timeout= */5000)
+
+ // Clean up
+ activityRule.runOnUiThread {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt
new file mode 100644
index 0000000..d5f5fa1
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 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 androidx.appcompat.app
+
+import android.app.Activity
+import android.app.Instrumentation
+import androidx.appcompat.Orientation
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.withOrientation
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.LifecycleOwnerUtils
+import org.junit.After
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 18, maxSdkVersion = 31)
+class LocalesRotateRecreatesActivityWithConfigTestCase() {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ @get:Rule
+ public val activityRule: LocalesActivityTestRule<LocalesUpdateActivity> =
+ LocalesActivityTestRule(
+ LocalesUpdateActivity::class.java,
+ initialTouchMode = false,
+ // Let the test method launch its own activity so that we can ensure it's RESUMED.
+ launchActivity = false
+ )
+
+ @After
+ public fun teardown() {
+ device.setOrientationNatural()
+ device.waitForIdle(/* timeout= */5000)
+
+ // Clean up after the default mode test.
+
+ activityRule.runOnUiThread {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+ }
+
+ @Test
+ public fun testRotateRecreatesActivityWithConfig() {
+ // Set locales to CUSTOM_LOCALE_LIST and wait for state RESUMED.
+ val initialActivity = activityRule.launchActivity(null)
+ LifecycleOwnerUtils.waitUntilState(initialActivity, Lifecycle.State.RESUMED)
+ systemLocales = LocalesUpdateActivity.getConfigLocales(activityRule.activity.resources
+ .configuration)
+
+ setLocalesAndWaitForRecreate(initialActivity, CUSTOM_LOCALE_LIST)
+
+ val localesActivity = activityRule.activity
+ val config = localesActivity.resources.configuration
+
+ // On API level 26 and below, the configuration object is going to be identical
+ // across configuration changes, so we need to pull the orientation value now.
+ val orientation = config.orientation
+
+ // Assert that the current Activity has the expected locales.
+ assertConfigurationLocalesEquals(LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales), config)
+
+ // Now rotate the device. This should result in an onDestroy lifecycle event.
+ localesActivity.resetOnDestroy()
+ rotateDeviceAndWaitForRecreate(localesActivity) {
+ localesActivity.expectOnDestroy(/* timeout= */ 5000)
+
+ // Assert that we got a different activity and thus it was recreated.
+ val rotatedLocalesActivity = activityRule.activity
+ val rotatedConfig = rotatedLocalesActivity.resources.configuration
+ assertNotSame(localesActivity, rotatedLocalesActivity)
+ assertConfigurationLocalesEquals(
+ LocalesUpdateActivity.overlayCustomAndSystemLocales(CUSTOM_LOCALE_LIST,
+ systemLocales),
+ rotatedConfig
+ )
+
+ // On API level 26 and below, the configuration object is going to be identical
+ // across configuration changes, so we need to compare against the cached value.
+ assertNotSame(orientation, rotatedConfig.orientation)
+ }
+ }
+
+ private fun rotateDeviceAndWaitForRecreate(activity: Activity, doThis: () -> Unit) {
+ val monitor = Instrumentation.ActivityMonitor(activity::class.java.name, /* result= */
+ null, /* block= */false)
+ instrumentation.addMonitor(monitor)
+
+ device.withOrientation(Orientation.LEFT) {
+ // Wait for the activity to be recreated after rotation
+ var count = 0
+ var lastActivity: Activity? = activity
+ while ((lastActivity == null || activity == lastActivity) && count < 5) {
+ // If this times out, it will return null.
+ lastActivity = monitor.waitForActivityWithTimeout(/* timeout= */ 1000L)
+ count++
+ }
+ instrumentation.waitForIdleSync()
+
+ // Ensure that we didn't time out
+ assertNotNull("Activity was not recreated within 5000ms", lastActivity)
+ assertNotEquals(
+ "Activity was not recreated within 5000ms", activity, lastActivity
+ )
+ doThis()
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt
new file mode 100644
index 0000000..82fd8e4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.os.LocaleList
+import androidx.annotation.RequiresApi
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.BuildCompat
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales. The minSdkVersion should be set to 33 after API bump.
+@SdkSuppress(minSdkVersion = 32)
+class LocalesSetUsingFrameworkApiTestCase {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @RequiresApi(33)
+ @Before
+ fun setUp() {
+ // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+ // bumped to 33
+ assumeTrue(
+ "Requires API version >=T", BuildCompat.isAtLeastT()
+ )
+
+ // setting the app to follow system.
+ AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication(),
+ LocaleList.getEmptyLocaleList()
+ )
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(
+ rule.activity.resources.configuration
+ )
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ LocalesUtils.CUSTOM_LOCALE_LIST, systemLocales
+ )
+ }
+
+ /**
+ * Verifies that for API version >=T the AppCompatDelegate.setApplicationLocales() call
+ * is redirected to the framework API and the locales are applied successfully.
+ */
+ @Test
+ @RequiresApi(33)
+ fun testSetApplicationLocales_postT_frameworkApiCalled() {
+ val firstActivity = rule.activity
+ assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+ assertEquals(
+ LocaleListCompat.getEmptyLocaleList(),
+ AppCompatDelegate.getApplicationLocales()
+ )
+ assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+ // Now change the locales for the activity
+ val recreatedFirst = LocalesUtils.setLocalesAndWaitForRecreate(
+ firstActivity,
+ CUSTOM_LOCALE_LIST
+ )
+
+ assertEquals(
+ CUSTOM_LOCALE_LIST,
+ AppCompatDelegate.getApplicationLocales()
+ )
+ // check that the locales were set using the framework API
+ assertEquals(
+ CUSTOM_LOCALE_LIST.toLanguageTags(),
+ AppCompatDelegate.Api33Impl.localeManagerGetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication()
+ ).toLanguageTags()
+ )
+ // check locales are applied successfully
+ assertConfigurationLocalesEquals(expectedLocales, recreatedFirst)
+ // check that the override was not done by AndroidX, but by the framework
+ assertNull(AppCompatDelegate.getRequestedAppLocales())
+ }
+
+ @RequiresApi(33)
+ @After
+ fun teardown() {
+ // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+ // bumped to 33
+ if (!BuildCompat.isAtLeastT()) {
+ return
+ }
+ // clearing locales from framework. setting the app to follow system.
+ AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication(),
+ LocaleList.getEmptyLocaleList()
+ )
+ }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt
new file mode 100644
index 0000000..a8cb155
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.app.Instrumentation.ActivityMonitor
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.Assert.assertNotNull
+import junit.framework.Assert.assertNotSame
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesStackedHandlingTestCase {
+
+ @Before
+ fun setUp() {
+ LocalesUtils.initCustomLocaleList()
+ }
+
+ /**
+ * Regression test for the following scenario:
+ *
+ * If you have a stack of activities which includes one with android:configChanges="locale" and
+ * android:configChanges="layoutDirection" and you call AppCompatDelegate.setApplicationLocales
+ * it can cause other activities to not be recreated.
+ *
+ * Eg:
+ * - Activity A DOESN'T intercept locales changes and layoutDirection changes in manifest
+ * - Activity B DOESN'T intercept locales changes and layoutDirection changes in manifest
+ * - Activity C DOES intercept both locales and layoutDirection changes in manifest
+ *
+ * Here is your stack : A > B > C (C on top)
+ *
+ * Call AppCompatDelegate.setApplicationLocales with a new mode on activity C. Activity C
+ * receives the change in onConfigurationChanged but there is a good chance that activity A
+ * and/or B were not recreated.
+ */
+ @Test
+ fun testLocalesWithStackedActivities() {
+ val instr = InstrumentationRegistry.getInstrumentation()
+ val result = Instrumentation.ActivityResult(0, Intent())
+ val monitorA = ActivityMonitor(LocalesActivityA::class.java.name, result, false)
+ val monitorB = ActivityMonitor(LocalesActivityB::class.java.name, result, false)
+ val monitorC = ActivityMonitor(
+ LocalesConfigChangesActivity::class.java.name,
+ result, false
+ )
+ instr.addMonitor(monitorA)
+ instr.addMonitor(monitorB)
+ instr.addMonitor(monitorC)
+
+ instr.runOnMainSync {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+
+ // Start activity A.
+ instr.startActivitySync(
+ Intent(instr.context, LocalesActivityA::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "A")
+ }
+ )
+
+ // From activity A, start activity B.
+ val activityA = monitorA.waitForActivityWithTimeout(/* timeout= */ 3000)
+ as LocalesUpdateActivity
+ assertNotNull(activityA)
+ activityA.startActivity(
+ Intent(instr.context, LocalesActivityB::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "B")
+ }
+ )
+
+ var systemLocales = LocalesUpdateActivity.getConfigLocales(
+ activityA.resources.configuration
+ )
+
+ // Activity A is hidden, wait for it to stop.
+ waitUntilState(activityA, Lifecycle.State.CREATED)
+
+ // From activity B, start activity C.
+ val activityB =
+ monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotNull(activityB)
+ activityB.startActivity(
+ Intent(instr.context, LocalesConfigChangesActivity::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "C")
+ }
+ )
+
+ // Activity B is hidden, wait for it to stop.
+ waitUntilState(activityB, Lifecycle.State.CREATED)
+
+ // apply CUSTOM_LOCALE_LIST
+ val activityC =
+ monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotNull(activityC)
+ activityC.runOnUiThread {
+ AppCompatDelegate.setApplicationLocales(CUSTOM_LOCALE_LIST)
+ }
+
+ // Activity C should receive a configuration change.
+ activityC.expectOnConfigurationChange(/* timeout= */ 3000)
+
+ // Activities A and B should recreate() in the background.
+ val activityA2 = expectRecreate(monitorA, activityA) as LocalesUpdateActivity
+ val activityB2 = expectRecreate(monitorB, activityB) as LocalesUpdateActivity
+
+ var expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales
+ )
+ // Activity C should have received a locales configuration change.
+ listOf(activityC, activityA2, activityB2).forEach { activity ->
+ activityC.runOnUiThread {
+ assertConfigurationLocalesEquals(
+ "Activity ${activity.title}'s effective configuration has locales set",
+ expectedLocales,
+ activityC.effectiveConfiguration!!
+ )
+ }
+ }
+ }
+
+ /**
+ * Regression test for the following scenario:
+ *
+ * If you have a stack of activities where every activity has `android:configChanges="locale"`
+ * and android:configChanges="layoutDirection" and you call
+ * [AppCompatDelegate.setApplicationLocales] from thread other than the top activity,
+ * then it can cause the bottom activity to not receive `onConfigurationChanged`.
+ *
+ * Eg:
+ * - Activity A DOES intercept locales and layoutDirection changes in manifest
+ * - Activity B DOES intercept locales and layoutDirection changes in manifest
+ * - Activity C DOES intercept locales and layoutDirection changes in manifest
+ *
+ * Here is your stack : A > B > C (C on top)
+ *
+ * Call [AppCompatDelegate.setApplicationLocales] with a new mode on activity C (but not
+ * directly
+ * from this activity, ex with RX AndroidSchedulers.mainThread or an handler). Activity C
+ * receives both `onConfigurationChanged` and `onLocalesChanged`, but activities A and B
+ * may not receive either callback or change their configurations.
+ *
+ * Process:
+ * 1. A > B > C > setApplicationLocales YES
+ * 2. Go back to A (B & C destroyed) > B > C > setApplicationLocales NO (wrong config for A)
+ * 3. repeat (YES/NO/YES/NO...)
+ */
+ @Test
+ fun testLocalesWithStackedActivitiesAndNavigation() {
+ val instr = InstrumentationRegistry.getInstrumentation()
+ val result = Instrumentation.ActivityResult(0, Intent())
+ val monitorA = ActivityMonitor(
+ LocalesConfigChangesActivity::class.java.name,
+ result, false
+ )
+ val monitorB = ActivityMonitor(
+ LocalesConfigChangesActivityA::class.java.name,
+ result, false
+ )
+ val monitorC = ActivityMonitor(
+ LocalesConfigChangesActivityB::class.java.name,
+ result, false
+ )
+ instr.addMonitor(monitorA)
+ instr.addMonitor(monitorB)
+ instr.addMonitor(monitorC)
+
+ instr.runOnMainSync {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+
+ // Start activity A.
+ instr.startActivitySync(
+ Intent(instr.context, LocalesConfigChangesActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "A")
+ }
+ )
+
+ // From activity A, start activity B.
+ val activityA =
+ monitorA.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotNull("Activity A started within 3000ms", activityA)
+ activityA.startActivity(
+ Intent(instr.context, LocalesConfigChangesActivityA::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "B")
+ }
+ )
+
+ var systemLocales = LocalesUpdateActivity.getConfigLocales(
+ activityA.resources.configuration
+ )
+
+ // Activity A is hidden, wait for it to stop.
+ waitUntilState(activityA, Lifecycle.State.CREATED)
+
+ // From activity B, start activity C.
+ val activityB =
+ monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotNull("Activity B started within 3000ms", activityB)
+ activityB.startActivity(
+ Intent(instr.context, LocalesConfigChangesActivityB::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "C")
+ }
+ )
+
+ // Activity B is hidden, wait for it to stop.
+ waitUntilState(activityB, Lifecycle.State.CREATED)
+
+ // Wait for activity C to start.
+ val activityC =
+ monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotNull("Activity C started within 3000ms", activityC)
+
+ // Change locales from a non-UI thread.
+ Handler(Looper.getMainLooper()).post {
+ AppCompatDelegate.setApplicationLocales(CUSTOM_LOCALE_LIST)
+ }
+
+ // Activities A, B, and C should all receive configuration changes.
+ listOf(activityA, activityB, activityC).forEach { activity ->
+ activity.expectOnConfigurationChange(/* timeout= */ 3000)
+ }
+
+ var expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales
+ )
+
+ // Activities A, B, and C should have all received the new configuration.
+ listOf(activityA, activityB, activityC).forEach { activity ->
+ activity.runOnUiThread {
+ assertConfigurationLocalesEquals(
+ "Activity ${activity.title}'s effective configuration has locales set",
+ expectedLocales,
+ activity.effectiveConfiguration!!
+ )
+ }
+ }
+
+ // Tear down activities C and B, in that order.
+ listOf(activityC, activityB).forEach { activity ->
+ activity.runOnUiThread {
+ activity.finish()
+ }
+ waitUntilState(activity, Lifecycle.State.DESTROYED)
+ }
+
+ // Activity A is in the foreground, wait for it to resume.
+ waitUntilState(activityA, Lifecycle.State.RESUMED)
+
+ // From activity A, start activity B again.
+ activityA.startActivity(
+ Intent(instr.context, LocalesConfigChangesActivityA::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "B2")
+ }
+ )
+
+ // Activity A is hidden, wait for it to stop.
+ waitUntilState(activityA, Lifecycle.State.CREATED)
+
+ // From activity B, start activity C. Double-check the return, since the monitor could
+ // trigger on Activity B's lifecycle if the platform does something unexpected.
+ val activityB2 =
+ monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotSame("Monitor responded to activity B2 lifecycle", activityB, activityB2)
+ assertNotNull("Activity B2 started within 3000ms", activityB2)
+ activityB2.startActivity(
+ Intent(instr.context, LocalesConfigChangesActivityB::class.java).apply {
+ putExtra(LocalesUpdateActivity.KEY_TITLE, "C2")
+ }
+ )
+
+ // Activity B is hidden, wait for it to stop.
+ waitUntilState(activityB2, Lifecycle.State.CREATED)
+
+ // Wait for activity C to start. Double-check the return.
+ val activityC2 =
+ monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+ assertNotSame("Monitor responded to Activity C2 lifecycle", activityC, activityC2)
+ assertNotNull("Activity C2 started within 3000ms", activityC2)
+
+ // Prepare activities A, B, and C to track configuration changes.
+ listOf(activityA, activityB2, activityC2).forEach { activity ->
+ activity.resetOnConfigurationChange()
+ }
+
+ // Change locales again from a non-UI thread.
+ Handler(Looper.getMainLooper()).post {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+
+ // Activities A, B, and C should all receive configuration changes.
+ listOf(activityA, activityB2, activityC2).forEach { activity ->
+ activity.expectOnConfigurationChange(/* timeout= */ 3000)
+ }
+
+ // Activities A, B, and C should have all received the new configuration.
+ listOf(activityA, activityB2, activityC2).forEach { activity ->
+ activity.runOnUiThread {
+ assertConfigurationLocalesEquals(
+ "Activity ${activity.title}'s effective configuration has locales set",
+ systemLocales,
+ activity.effectiveConfiguration!!
+ )
+ }
+ }
+ }
+
+ private fun expectRecreate(monitor: ActivityMonitor, activity: Activity): Activity {
+ // The documentation says "Block until an Activity is created that matches this monitor."
+ // This statement is true, but there are some other true statements like: "Block until an
+ // Activity is destroyed" or "Block until an Activity is resumed"...
+ var activityResult: Activity?
+ synchronized(monitor) {
+ do {
+ // this call will release synchronization monitor's monitor
+ activityResult = monitor.waitForActivityWithTimeout(/* timeout= */ 3000)
+ } while (activityResult != null && activityResult == activity)
+ }
+
+ assertNotNull("Recreated activity " + activity.title, activityResult)
+ return activityResult!!
+ }
+
+ @After
+ fun teardown() {
+ LocalesUpdateActivity.teardown()
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
new file mode 100644
index 0000000..7c51c8b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.LocaleList
+import androidx.annotation.RequiresApi
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.BuildCompat
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test case to verify app-locales sync to framework on Version upgrade from Pre T to T.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+// This test should only be run for API version T and hence after API bump both
+// minSdkVersion and maxSdkVersion should be set to 33.
+@SdkSuppress(minSdkVersion = 32, maxSdkVersion = 33)
+class LocalesSyncToFrameworkTestCase {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+ private lateinit var appLocalesComponent: ComponentName
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ @RequiresApi(33)
+ @Before
+ fun setUp() {
+ // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+ // bumped to 33
+ assumeTrue("Requires API version >=T", BuildCompat.isAtLeastT())
+ // setting the app to follow system.
+ AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication(),
+ LocaleList.getEmptyLocaleList()
+ )
+
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(
+ rule.activity.resources.configuration
+ )
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+ CUSTOM_LOCALE_LIST, systemLocales
+ )
+
+ appLocalesComponent = ComponentName(
+ instrumentation.context,
+ AppLocalesStorageHelper.APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME
+ )
+ }
+
+ @RequiresApi(33)
+ @Test
+ fun testAutoSync_preTToPostT_syncsSuccessfully() {
+ val firstActivity = rule.activity
+
+ // activity is following the system and the requested locales are null.
+ assertConfigurationLocalesEquals(systemLocales, firstActivity)
+ assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+ val context = instrumentation.context
+
+ // persist some app locales in storage, mimicking locales set using the backward
+ // compatibility API
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+ AppLocalesStorageHelper.persistLocales(context, CUSTOM_LOCALE_LIST.toLanguageTags())
+
+ // explicitly disable appLocalesComponent that acts as a marker to represent that the
+ // locales has been synced so that when a new activity is created the locales are
+ // synced from storage
+ context.packageManager.setComponentEnabledSetting(
+ appLocalesComponent,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ /* flags= */ PackageManager.DONT_KILL_APP
+ )
+
+ // resetting static storage represents a fresh app start up.
+ AppCompatDelegate.resetStaticRequestedAndStoredLocales()
+
+ // Start a new Activity, so that the original Activity goes into the background
+ val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+ // wait for locales to get synced, stop execution of the current thread for the
+ // timeout period
+ Thread.sleep(/* timeout= */ 1000)
+
+ // check that the locales were set using the framework API and they have been synced
+ // successfully
+ assertEquals(
+ CUSTOM_LOCALE_LIST.toLanguageTags(),
+ AppCompatDelegate.Api33Impl.localeManagerGetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication()
+ ).toLanguageTags()
+ )
+ // check that the activity has the app specific locales
+ assertConfigurationLocalesEquals(expectedLocales, secondActivity)
+ // check that the override was not done by AndroidX, but by the framework
+ assertNull(AppCompatDelegate.getRequestedAppLocales())
+ // check that the synced marker was set to true
+ assertEquals(
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ context.packageManager.getComponentEnabledSetting(appLocalesComponent)
+ )
+
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+ }
+
+ @After
+ @RequiresApi(33)
+ fun teardown() {
+ // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+ // bumped to 33
+ if (!BuildCompat.isAtLeastT()) {
+ return
+ }
+ val context = instrumentation.context
+
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+ // setting empty locales deletes the persisted locales record.
+ AppLocalesStorageHelper.persistLocales(context, /* empty locales */ "")
+ AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+
+ // clearing locales from framework.
+ // setting the app to follow system.
+ AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+ AppCompatDelegate.getLocaleManagerForApplication(),
+ LocaleList.getEmptyLocaleList()
+ )
+
+ // disabling component enabled setting for app_locales sync marker.
+ context.packageManager.setComponentEnabledSetting(
+ appLocalesComponent,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ /* flags= */ PackageManager.DONT_KILL_APP
+ )
+ }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java
new file mode 100644
index 0000000..7678876
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 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 androidx.appcompat.app;
+
+import static androidx.appcompat.app.LocaleOverlayHelper.combineLocalesIfOverlayExists;
+
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.test.R;
+import androidx.appcompat.testutils.BaseTestActivity;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class LocalesUpdateActivity extends BaseTestActivity {
+
+
+ public static final String KEY_TITLE = "title";
+
+ private final Semaphore mOnConfigurationChangeSemaphore = new Semaphore(0);
+ private final Semaphore mOnDestroySemaphore = new Semaphore(0);
+ private final Semaphore mOnCreateSemaphore = new Semaphore(0);
+
+ private LocaleListCompat mLastLocales = LocaleListCompat.getEmptyLocaleList();
+
+ private Configuration mEffectiveConfiguration;
+ private Configuration mLastConfigurationChange;
+
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.activity_locales;
+ }
+
+ @Override
+ public void onLocalesChanged(@NonNull LocaleListCompat locales) {
+ mLastLocales = locales;
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mLastConfigurationChange = new Configuration(newConfig);
+ mEffectiveConfiguration = mLastConfigurationChange;
+ mOnConfigurationChangeSemaphore.release();
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ String title = getIntent().getStringExtra(KEY_TITLE);
+ if (title != null) {
+ setTitle(title);
+ }
+
+ mEffectiveConfiguration = new Configuration(getResources().getConfiguration());
+ mOnCreateSemaphore.release();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ mOnDestroySemaphore.release();
+ }
+
+ @Nullable
+ Configuration getLastConfigurationChangeAndClear() {
+ final Configuration config = mLastConfigurationChange;
+ mLastConfigurationChange = null;
+ return config;
+ }
+
+ /**
+ * @return a copy of the {@link Configuration} from the most recent call to {@link #onCreate} or
+ * {@link #onConfigurationChanged}, or {@code null} if neither has been called yet
+ */
+ @Nullable
+ Configuration getEffectiveConfiguration() {
+ return mEffectiveConfiguration;
+ }
+
+ LocaleListCompat getLastLocalesAndReset() {
+ final LocaleListCompat locales = mLastLocales;
+ mLastLocales = LocaleListCompat.getEmptyLocaleList();
+ return locales;
+ }
+
+ public static LocaleListCompat getConfigLocales(Configuration conf) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return AppCompatDelegateImpl.Api24Impl.getLocales(conf);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ return LocaleListCompat.forLanguageTags(AppCompatDelegateImpl.Api21Impl
+ .toLanguageTag(conf.locale));
+ } else {
+ return LocaleListCompat.create(conf.locale);
+ }
+ }
+
+ public static LocaleListCompat overlayCustomAndSystemLocales(LocaleListCompat customLocales,
+ LocaleListCompat baseLocales) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return combineLocalesIfOverlayExists(customLocales, baseLocales);
+ } else {
+ return LocaleListCompat.create(customLocales.get(0));
+ }
+ }
+
+ /**
+ * Resets the number of received configuration changes.
+ * <p>
+ * Call this method before {@link #expectOnConfigurationChange(long)} to ensure only future
+ * configuration changes are counted.
+ */
+ public void resetOnConfigurationChange() {
+ mOnConfigurationChangeSemaphore.drainPermits();
+ }
+
+ /**
+ * Blocks until a single configuration change has been received.
+ * <p>
+ * Configuration changes are sticky; if any configuration changes were received prior to
+ * calling this method and {@link #resetOnConfigurationChange()} has not been called, this
+ * method will return immediately.
+ *
+ * @param timeout maximum amount of time (in ms) to wait for a configuration change
+ * @throws InterruptedException if the lock is interrupted
+ */
+ public void expectOnConfigurationChange(long timeout) throws InterruptedException {
+ if (Thread.currentThread() == getMainLooper().getThread()) {
+ throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+ }
+
+ mOnConfigurationChangeSemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Resets the number of received onCreate lifecycle events.
+ * <p>
+ * Call this method before {@link #expectOnCreate(long)} to ensure only future
+ * onCreate lifecycle events are counted.
+ */
+ public void resetOnCreate() {
+ mOnCreateSemaphore.drainPermits();
+ }
+
+ /**
+ * Blocks until a single onCreate lifecycle event has been received.
+ * <p>
+ * Lifecycle events are sticky; if any events were received prior to calling this method and
+ * an event has been received, this method will return immediately.
+ *
+ * @param timeout maximum amount of time (in ms) to wait for an onCreate event
+ * @throws InterruptedException if the lock is interrupted
+ */
+ public void expectOnCreate(long timeout) throws InterruptedException {
+ if (Thread.currentThread() == getMainLooper().getThread()) {
+ throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+ }
+
+ mOnCreateSemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Resets the number of received onDestroy lifecycle events.
+ * <p>
+ * Call this method before {@link #expectOnDestroy(long)} to ensure only future
+ * onDestroy lifecycle events are counted.
+ */
+ public void resetOnDestroy() {
+ mOnDestroySemaphore.drainPermits();
+ }
+
+ /**
+ * Blocks until a single onDestroy lifecycle event has been received.
+ * <p>
+ * Lifecycle events are sticky; if any events were received prior to calling this method and
+ * an event has been received, this method will return immediately.
+ *
+ * @param timeout maximum amount of time (in ms) to wait for an onDestroy event
+ * @throws InterruptedException if the lock is interrupted
+ */
+ public void expectOnDestroy(long timeout) throws InterruptedException {
+ if (Thread.currentThread() == getMainLooper().getThread()) {
+ throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+ }
+
+ mOnDestroySemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Teardown method to clean up persistent locales in static storage.
+ */
+ public static void teardown() {
+ AppCompatDelegate.resetStaticRequestedAndStoredLocales();
+ }
+
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
new file mode 100644
index 0000000..d504dc6
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
@@ -0,0 +1,118 @@
+/*
+ * 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 androidx.appcompat.app
+
+import android.webkit.WebView
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWait
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.waitForExecution
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesUpdateTestCase() {
+ @get:Rule
+ val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+ var systemLocales = LocaleListCompat.getEmptyLocaleList()
+ var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+ @Before
+ fun setUp() {
+ // Since no locales are applied as of now, current configuration will have system
+ // locales.
+ systemLocales = LocalesUpdateActivity.getConfigLocales(rule.activity
+ .resources.configuration)
+ expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(CUSTOM_LOCALE_LIST,
+ systemLocales)
+ }
+ @Test
+ fun testDialogDoesNotOverrideActivityConfiguration() {
+ setLocalesAndWaitForRecreate(rule, CUSTOM_LOCALE_LIST)
+ // Now show a AppCompatDialog
+ lateinit var dialog: AppCompatDialog
+ rule.runOnUiThread {
+ dialog = AppCompatDialog(rule.activity)
+ dialog.show()
+ }
+ rule.waitForExecution()
+ // Now dismiss the dialog
+ rule.runOnUiThread { dialog.dismiss() }
+
+ // Assert that the locales are unchanged
+ assertConfigurationLocalesEquals(
+ expectedLocales,
+ rule.activity.resources.configuration
+ )
+ }
+
+ @Test
+ fun testLoadingWebViewMaintainsConfiguration() {
+ setLocalesAndWaitForRecreate(rule, CUSTOM_LOCALE_LIST)
+
+ // Now load a WebView into the Activity
+ rule.runOnUiThread { WebView(rule.activity) }
+
+ // Now assert that the context still has applied locales.
+ assertEquals(
+ expectedLocales,
+ LocalesUpdateActivity.getConfigLocales(rule.activity.resources.configuration)
+ )
+ }
+
+ @Test
+ fun testOnLocalesChangedCalled() {
+ val activity = rule.activity
+ // Set local night mode to YES
+ setLocalesAndWait(rule, CUSTOM_LOCALE_LIST)
+ // Assert that the Activity received a new value
+ assertEquals(expectedLocales, activity.lastLocalesAndReset)
+ }
+
+ @Test
+ fun testOnConfigurationChangeNotCalled() {
+ var activity = rule.activity
+ // Set locales to CUSTOM_LOCALE_LIST.
+ LocalesUtils.setLocalesAndWait(
+ rule,
+ CUSTOM_LOCALE_LIST
+ )
+ // Assert that onConfigurationChange was not called on the original activity
+ assertNull(activity.lastConfigurationChangeAndClear)
+
+ activity = rule.activity
+ // Set locales back to system locales.
+ setLocalesAndWait(
+ rule,
+ LocaleListCompat.getEmptyLocaleList()
+ )
+ // Assert that onConfigurationChange was not called
+ assertNull(activity.lastConfigurationChangeAndClear)
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
index 2ccfc39..a0e190b 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
@@ -44,7 +44,7 @@
// Open a dialog on top of the activity.
rule.runOnUiThread {
- val frag = NightModeDialogFragment.newInstance()
+ val frag = TestDialogFragment.newInstance()
frag.show(firstActivity.supportFragmentManager, "dialog")
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.java
similarity index 70%
rename from appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java
rename to appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.java
index ba3932e..907c068 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 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.
@@ -23,22 +23,26 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
-public class NightModeDialogFragment extends DialogFragment {
+/**
+ * Test class extending DialogFragment used for testing of configuration changes like nightMode and
+ * locales.
+ */
+public class TestDialogFragment extends DialogFragment {
- public NightModeDialogFragment() {
+ public TestDialogFragment() {
// Public empty constructor used to handle lifecycle events.
}
- public static NightModeDialogFragment newInstance() {
- return new NightModeDialogFragment();
+ public static TestDialogFragment newInstance() {
+ return new TestDialogFragment();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
- builder.setTitle("NightModeDialogFragment");
- builder.setMessage("NightModeDialogFragment");
+ builder.setTitle("TestDialogFragment");
+ builder.setMessage("TestDialogFragment");
return builder.create();
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt
new file mode 100644
index 0000000..976fac7
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 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 androidx.appcompat.testutils
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.LocalesUpdateActivity
+import androidx.appcompat.testutils.LocalesUtils.initCustomLocaleList
+import androidx.core.os.LocaleListCompat
+
+@Suppress("DEPRECATION")
+class LocalesActivityTestRule<T : AppCompatActivity>(
+ activityClazz: Class<T>,
+ initialTouchMode: Boolean = false,
+ launchActivity: Boolean = true
+) : androidx.test.rule.ActivityTestRule<T>(activityClazz, initialTouchMode, launchActivity) {
+ override fun beforeActivityLaunched() {
+ initCustomLocaleList()
+ // By default we'll set the locales to match system locales, which allows us to make better
+ // assumptions in the test below.
+ runOnUiThread {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ }
+ }
+
+ override fun afterActivityFinished() {
+ // Reset locales persisted in static storage.
+ LocalesUpdateActivity.teardown()
+ }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt
new file mode 100644
index 0000000..36dd7ea
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 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 androidx.appcompat.testutils
+
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import android.util.Log
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils
+import androidx.testutils.PollingCheck
+import java.util.Locale
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+
+object LocalesUtils {
+ private const val LOG_TAG = "LocalesUtils"
+
+ /**
+ * A test {@link LocaleListCompat} containing locales [CANADA_FRENCH, CHINESE].
+ */
+ var CUSTOM_LOCALE_LIST: LocaleListCompat = LocaleListCompat.getEmptyLocaleList()
+
+ fun initCustomLocaleList() {
+ if (Build.VERSION.SDK_INT >= 24) {
+ CUSTOM_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+ Locale.CANADA_FRENCH.toLanguageTag() + "," +
+ Locale.CHINESE.toLanguageTag()
+ )
+ } else {
+ CUSTOM_LOCALE_LIST = LocaleListCompat.create(Locale.CHINESE)
+ }
+ }
+
+ fun assertConfigurationLocalesEquals(
+ expectedLocales: LocaleListCompat,
+ context: Context
+ ) {
+ assertConfigurationLocalesEquals(
+ null,
+ expectedLocales,
+ context
+ )
+ }
+
+ fun assertConfigurationLocalesEquals(
+ message: String?,
+ expectedLocales: LocaleListCompat,
+ context: Context
+ ) {
+ assertConfigurationLocalesEquals(
+ message,
+ expectedLocales,
+ context.resources.configuration
+ )
+ }
+
+ fun assertConfigurationLocalesEquals(
+ expectedLocales: LocaleListCompat,
+ configuration: Configuration
+ ) {
+ assertConfigurationLocalesEquals(
+ null,
+ expectedLocales,
+ configuration
+ )
+ }
+
+ fun assertConfigurationLocalesEquals(
+ message: String?,
+ expectedLocales: LocaleListCompat,
+ configuration: Configuration
+ ) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ assertEquals(
+ message,
+ expectedLocales.toLanguageTags(),
+ configuration.locales.toLanguageTags()
+ )
+ } else {
+ assertEquals(
+ message,
+ expectedLocales.get(0),
+ @Suppress("DEPRECATION") configuration.locale
+ )
+ }
+ }
+
+ fun <T : AppCompatActivity> setLocalesAndWait(
+ @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+ locales: LocaleListCompat
+ ) {
+ setLocalesAndWait(activityRule.activity, activityRule, locales)
+ }
+
+ fun <T : AppCompatActivity> setLocalesAndWait(
+ activity: AppCompatActivity?,
+ @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+ locales: LocaleListCompat
+ ) {
+ Log.d(
+ LOG_TAG,
+ "setLocalesAndWait on Activity: " + activity +
+ " to locales: " + locales
+ )
+
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ activityRule.runOnUiThread { setLocales(locales) }
+ instrumentation.waitForIdleSync()
+ }
+
+ fun <T : AppCompatActivity> setLocalesAndWaitForRecreate(
+ @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+ locales: LocaleListCompat
+ ): T = setLocalesAndWaitForRecreate(activityRule.activity, locales)
+
+ fun <T : AppCompatActivity> setLocalesAndWaitForRecreate(
+ activity: T,
+ locales: LocaleListCompat
+ ): T {
+ Log.d(
+ LOG_TAG,
+ "setLocalesAndWaitForRecreate on Activity: " + activity +
+ " to mode: " + locales
+ )
+
+ LifecycleOwnerUtils.waitUntilState(activity, Lifecycle.State.RESUMED)
+
+ // Screen rotation kicks off a lot of background work, so we might need to wait a bit
+ // between the activity reaching RESUMED state and it actually being shown on screen.
+ PollingCheck.waitFor {
+ activity.hasWindowFocus()
+ }
+ assertNotEquals(locales, getLocales())
+
+ // Now perform locales change and wait for the Activity to be recreated.
+ return LifecycleOwnerUtils.waitForRecreation(activity) {
+ setLocales(locales)
+ }
+ }
+
+ fun setLocales(
+ locales: LocaleListCompat
+ ) = AppCompatDelegate.setApplicationLocales(locales)
+
+ private fun getLocales(): LocaleListCompat = AppCompatDelegate.getApplicationLocales()
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/res/layout/activity_locales.xml b/appcompat/appcompat/src/androidTest/res/layout/activity_locales.xml
new file mode 100644
index 0000000..cbbdf80
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/activity_locales.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/text_locales"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="locales" />
+
+ <WebView
+ android:id="@+id/webView"
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:layout_gravity="bottom" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
index 981c192..41c34c9 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
@@ -45,6 +45,7 @@
import androidx.core.app.ActivityCompat;
import androidx.core.app.NavUtils;
import androidx.core.app.TaskStackBuilder;
+import androidx.core.os.LocaleListCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewTreeLifecycleOwner;
import androidx.lifecycle.ViewTreeViewModelStoreOwner;
@@ -662,4 +663,13 @@
*/
protected void onNightModeChanged(@NightMode int mode) {
}
+
+ /**
+ * Called when the locales have been changed. See {@link AppCompatDelegate#applyAppLocales()}
+ * for more information.
+ *
+ * @param locales the localeListCompat which has been applied
+ */
+ protected void onLocalesChanged(@NonNull LocaleListCompat locales) {
+ }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index fc04398..1fc0287 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -17,12 +17,21 @@
package androidx.appcompat.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.appcompat.app.AppLocalesStorageHelper.persistLocales;
+import static androidx.appcompat.app.AppLocalesStorageHelper.readLocales;
+import static androidx.appcompat.app.AppLocalesStorageHelper.syncLocalesToFramework;
+
+import static java.util.Objects.requireNonNull;
import android.app.Activity;
import android.app.Dialog;
+import android.app.LocaleManager;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.os.LocaleList;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MenuInflater;
@@ -30,19 +39,25 @@
import android.view.ViewGroup;
import android.view.Window;
+import androidx.annotation.AnyThread;
import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.VectorEnabledTintResources;
import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
+import androidx.core.os.LocaleListCompat;
import androidx.core.view.WindowCompat;
import androidx.fragment.app.FragmentActivity;
@@ -95,6 +110,10 @@
static final boolean DEBUG = false;
static final String TAG = "AppCompatDelegate";
+ static AppLocalesStorageHelper.SerialExecutor sSerialExecutorForLocalesStorage = new
+ AppLocalesStorageHelper.SerialExecutor(
+ new AppLocalesStorageHelper.ThreadPerTaskExecutor());
+
/**
* Mode which uses the system's night mode setting to determine if it is night or not.
*
@@ -165,6 +184,13 @@
@NightMode
private static int sDefaultNightMode = MODE_NIGHT_UNSPECIFIED;
+ private static LocaleListCompat sRequestedAppLocales = null;
+ private static LocaleListCompat sStoredAppLocales = null;
+ private static Boolean sIsAutoStoreLocalesOptedIn = null;
+ private static boolean sIsFrameworkSyncChecked = false;
+ private static Object sLocaleManager = null;
+ private static Context sAppContext = null;
+
/**
* All AppCompatDelegate instances associated with a "live" Activity, e.g. lifecycle state is
* post-onCreate and pre-onDestroy. These instances are used to instrument night mode's uiMode
@@ -173,6 +199,7 @@
private static final ArraySet<WeakReference<AppCompatDelegate>> sActivityDelegates =
new ArraySet<>();
private static final Object sActivityDelegatesLock = new Object();
+ private static final Object sAppLocalesStorageSyncLock = new Object();
/** @hide */
@SuppressWarnings("deprecation")
@@ -521,6 +548,31 @@
public abstract boolean applyDayNight();
/**
+ * Applies the current locales to this delegate's host component.
+ *
+ * <p>Apps can be notified when the locales are changed by overriding the
+ * {@link AppCompatActivity#onLocalesChanged(LocaleListCompat)} method.</p>
+ *
+ * <p>This is a default implementation and it is overridden atin
+ * {@link AppCompatDelegateImpl#applyAppLocales()} </p>
+ *
+ * @see #setApplicationLocales(LocaleListCompat)
+ *
+ * @return true if requested app-specific locales were applied, false if not.
+ */
+ boolean applyAppLocales() {
+ return false;
+ }
+
+ /**
+ * Returns the context for the current delegate.
+ */
+ @Nullable
+ public Context getContextForDelegate() {
+ return null;
+ }
+
+ /**
* Override the night mode used for this delegate's host component.
*
* <p>When setting a mode to be used across an entire app, the
@@ -595,6 +647,111 @@
}
/**
+ * Sets the current locales for the calling app.
+ *
+ * <p>If this method is called after any host components with attached
+ * {@link AppCompatDelegate}s have been 'created', a {@link LocaleList} configuration
+ * change will occur in each. This may result in those components being recreated, depending
+ * on their manifest configuration.</p>
+ *
+ * <p>This method accepts {@link LocaleListCompat} as an input parameter.</p>
+ *
+ * <p>Apps should continue to read Locales via their in-process {@link LocaleList}s.</p>
+ *
+ * <p>On API level 33 and above, this API will handle storage automatically.</p>
+ *
+ * <p>For API levels below that, the developer has two options:</p>
+ * <ul>
+ * <li>They can opt-in to automatic storage handled through the library. They can do this by
+ * adding a special metaData entry in their {@code AndroidManifest.xml}, similar to :
+ * <pre><code>
+ * <service
+ * android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+ * android:enabled="false"
+ * android:exported="false">
+ * <meta-data
+ * android:name="autoStoreLocales"
+ * android:value="true" />
+ * </service>
+ * </code></pre>
+ * They should be mindful that this will cause a blocking diskRead and diskWrite
+ * strictMode violation, and they might need to suppress it at their end.</li>
+ *
+ * <li>The second option is that they can choose to handle storage themselves. In order to
+ * do so they must use this API to initialize locales during app-start up and provide
+ * their stored locales. In this case, API should be called before Activity.onCreate()
+ * in the activity lifecycle, e.g. in attachBaseContext().
+ * <b>Note: Developers should gate this to API versions < 33.</b></li>
+ * </ul>
+ *
+ * <p>When the application using this API with API versions < 33 updates to a
+ * version >= 33, then there can be two scenarios for this transition:
+ * <ul>
+ * <li>If the developer has opted-in for autoStorage then the locales will be automatically
+ * synced to the framework. Developers must specify android:enabled="false" for the
+ * AppLocalesMetadataHolderService as shown in the meta-data entry above.</li>
+ * <li>If the developer has not opted-in for autoStorage then they will need to handle
+ * this transition on their end.</li>
+ * </ul>
+ *
+ * @param locales a list of locales.
+ */
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ public static void setApplicationLocales(@NonNull LocaleListCompat locales) {
+ requireNonNull(locales);
+ if (BuildCompat.isAtLeastT()) {
+ // If the API version is 33 (version for T) or above we want to redirect the call to
+ // the framework API.
+ Object localeManager = getLocaleManagerForApplication();
+ if (localeManager != null) {
+ Api33Impl.localeManagerSetApplicationLocales(localeManager,
+ Api24Impl.localeListForLanguageTags(locales.toLanguageTags()));
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, String.format("sRequestedAppLocales. New:%s, Current:%s",
+ locales, sRequestedAppLocales));
+ }
+ if (!locales.equals(sRequestedAppLocales)) {
+ synchronized (sActivityDelegatesLock) {
+ sRequestedAppLocales = locales;
+ applyLocalesToActiveDelegates();
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, String.format("Not applying changes, sRequestedAppLocales is already %s",
+ locales));
+ }
+ }
+ }
+
+ /**
+ * Returns application locales for the calling app as a {@link LocaleListCompat}.
+ *
+ * <p>Returns a {@link LocaleListCompat#getEmptyLocaleList()} if no app-specific locales are
+ * set.
+ */
+ @AnyThread
+ @NonNull
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ public static LocaleListCompat getApplicationLocales() {
+ if (BuildCompat.isAtLeastT()) {
+ // If the API version is 33 or above we want to redirect the call to the framework API.
+ Object localeManager = getLocaleManagerForApplication();
+ if (localeManager != null) {
+ return LocaleListCompat.wrap(Api33Impl.localeManagerGetApplicationLocales(
+ localeManager));
+ }
+ } else {
+ if (sRequestedAppLocales != null) {
+ // If app-specific locales exists then sRequestedApplicationLocales contains the
+ // latest locales.
+ return sRequestedAppLocales;
+ }
+ }
+ return LocaleListCompat.getEmptyLocaleList();
+ }
+
+ /**
* Returns the default night mode.
*
* @see #setDefaultNightMode(int)
@@ -605,6 +762,183 @@
}
/**
+ * Returns the requested app locales.
+ *
+ * @see #setApplicationLocales(LocaleListCompat)
+ */
+ @Nullable
+ static LocaleListCompat getRequestedAppLocales() {
+ return sRequestedAppLocales;
+ }
+
+ /**
+ * Returns the stored app locales.
+ *
+ * @see #setApplicationLocales(LocaleListCompat)
+ */
+ @Nullable
+ static LocaleListCompat getStoredAppLocales() {
+ return sStoredAppLocales;
+ }
+
+ /**
+ * Resets the static variables for requested and stored locales to null. This method is used
+ * for testing as it mimics activity restart which is difficult to do in a test.
+ */
+ @VisibleForTesting
+ static void resetStaticRequestedAndStoredLocales() {
+ sRequestedAppLocales = null;
+ sStoredAppLocales = null;
+ }
+
+ /**
+ * Sets {@link AppCompatDelegate#sIsAutoStoreLocalesOptedIn} to the provided value. This method
+ * is used for testing, setting sIsAutoStoreLocalesOptedIn to true mimics adding an opt-in
+ * "autoStoreLocales" meta-data entry.
+ *
+ * see {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}.
+ */
+ @VisibleForTesting
+ static void setIsAutoStoreLocalesOptedIn(boolean isAutoStoreLocalesOptedIn) {
+ sIsAutoStoreLocalesOptedIn = isAutoStoreLocalesOptedIn;
+ }
+
+ /**
+ * Returns the localeManager for the current application using active delegates to fetch
+ * context, returns null if no active delegates present.
+ */
+ @RequiresApi(33)
+ static Object getLocaleManagerForApplication() {
+ if (sLocaleManager != null) {
+ return sLocaleManager;
+ }
+ // Traversing through the active delegates to retrieve context for any one non null
+ // delegate.
+ // This context is used to create a localeManager which is saved as a static variable to
+ // reduce multiple object creation for different activities.
+ if (sAppContext == null) {
+ for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {
+ final AppCompatDelegate delegate = activeDelegate.get();
+ if (delegate != null) {
+ Context context = delegate.getContextForDelegate();
+ if (context != null) {
+ sAppContext = context;
+ break;
+ }
+ }
+ }
+ }
+
+ if (sAppContext != null) {
+ sLocaleManager = sAppContext.getSystemService(Context.LOCALE_SERVICE);
+ }
+ return sLocaleManager;
+ }
+
+ /**
+ * Returns true is the "autoStoreLocales" metaData is marked true in the app manifest.
+ */
+ static boolean isAutoStorageOptedIn(Context context) {
+ if (sIsAutoStoreLocalesOptedIn == null) {
+ try {
+ ServiceInfo serviceInfo = AppLocalesMetadataHolderService.getServiceInfo(
+ context);
+ if (serviceInfo.metaData != null) {
+ sIsAutoStoreLocalesOptedIn = serviceInfo.metaData.getBoolean(
+ /* key= */ "autoStoreLocales");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Checking for metadata for AppLocalesMetadataHolderService "
+ + ": Service not found");
+ sIsAutoStoreLocalesOptedIn = false;
+ }
+ }
+ return sIsAutoStoreLocalesOptedIn;
+ }
+
+ /**
+ * Executes {@link AppCompatDelegate#syncRequestedAndStoredLocales(Context)} asynchronously
+ * on a worker thread, serialized using {@link
+ * AppCompatDelegate#sSerialExecutorForLocalesStorage}.
+ *
+ * <p>This is done to perform the storage read operation without blocking the main thread.</p>
+ */
+ void asyncExecuteSyncRequestedAndStoredLocales(Context context) {
+ sSerialExecutorForLocalesStorage.execute(() -> syncRequestedAndStoredLocales(context));
+ }
+
+ /**
+ * Syncs requested and persisted app-specific locales.
+ *
+ * <p>This sync is only performed if the developer has opted in to use the autoStoredLocales
+ * feature, marked by the metaData "autoStoreLocales" wrapped in the service
+ * "AppLocalesMetadataHolderService". If the metaData is not found in the manifest or holds
+ * the value false then we return from this function without doing anything. If the metaData
+ * is set to true, then we perform a sync for app-locales.</p>
+ *
+ * <p>If the API version is >=33, then the storage is checked for app-specific locales, if
+ * found they are synced to the framework by calling the
+ * {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}</p>
+ *
+ * <p>If the API version is <33, then there are two scenarios:</p>
+ * <ul>
+ * <li>If the requestedAppLocales are not set then the app-specific locales are read from
+ * storage. If persisted app-specific locales are found then they are used to
+ * update the requestedAppLocales.</li>
+ * <li>If the requestedAppLocales are populated and are different from the stored locales
+ * then in that case the requestedAppLocales are stored and the static variable for
+ * storedAppLocales is updated accordingly.</li>
+ * </ul>
+ */
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ static void syncRequestedAndStoredLocales(Context context) {
+ if (!isAutoStorageOptedIn(context)) {
+ return;
+ } else if (BuildCompat.isAtLeastT()) {
+ // TODO: After BuildCompat.isAtLeast() is deprecated, the above condition needs to be
+ // replaced by (Build.VERSION.SDK_INT == 33).
+ if (!sIsFrameworkSyncChecked) {
+ // syncs locales from androidX to framework, it only happens once after the
+ // device is updated to T (API version 33).
+ sSerialExecutorForLocalesStorage.execute(() -> {
+ syncLocalesToFramework(context);
+ sIsFrameworkSyncChecked = true;
+ });
+ }
+ } else {
+ synchronized (sAppLocalesStorageSyncLock) {
+ if (sRequestedAppLocales == null) {
+ if (sStoredAppLocales == null) {
+ sStoredAppLocales =
+ LocaleListCompat.forLanguageTags(readLocales(context));
+ }
+ if (sStoredAppLocales.isEmpty()) {
+ // if both requestedLocales and storedLocales not set, then the user has not
+ // specified any application-specific locales. So no alterations in current
+ // application locales should take place.
+ return;
+ }
+ sRequestedAppLocales = sStoredAppLocales;
+ } else if (!sRequestedAppLocales.equals(sStoredAppLocales)) {
+ // if requestedLocales is set and is not equal to the storedLocales then in this
+ // case we need to store these locales in storage.
+ sStoredAppLocales = sRequestedAppLocales;
+ persistLocales(context, sRequestedAppLocales.toLanguageTags());
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Sets the value for {@link AppCompatDelegate#sAppContext} which is the context for the
+ * current application.
+ */
+ static void setAppContext(Context context) {
+ sAppContext = context;
+ }
+
+ /**
* Sets whether vector drawables on older platforms (< API 21) can be used within
* {@link android.graphics.drawable.DrawableContainer} resources.
*
@@ -695,4 +1029,48 @@
}
}
}
+
+ private static void applyLocalesToActiveDelegates() {
+ for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {
+ final AppCompatDelegate delegate = activeDelegate.get();
+ if (delegate != null) {
+ if (DEBUG) {
+ Log.d(TAG, "applyLocalesToActiveDelegates. Applying to " + delegate);
+ }
+ delegate.applyAppLocales();
+ }
+ }
+ }
+
+ @RequiresApi(24)
+ static class Api24Impl {
+ private Api24Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static LocaleList localeListForLanguageTags(String list) {
+ return LocaleList.forLanguageTags(list);
+ }
+ }
+
+ @RequiresApi(33)
+ static class Api33Impl {
+ private Api33Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void localeManagerSetApplicationLocales(Object localeManager,
+ LocaleList locales) {
+ LocaleManager mLocaleManager = (LocaleManager) localeManager;
+ mLocaleManager.setApplicationLocales(locales);
+ }
+
+ @DoNotInline
+ static LocaleList localeManagerGetApplicationLocales(Object localeManager) {
+ LocaleManager mLocaleManager = (LocaleManager) localeManager;
+ return mLocaleManager.getApplicationLocales();
+ }
+ }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index f42cc0b..bcdc36b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -24,6 +24,7 @@
import static android.view.Window.FEATURE_OPTIONS_PANEL;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.appcompat.app.LocaleOverlayHelper.combineLocalesIfOverlayExists;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -75,6 +76,7 @@
import android.widget.TextView;
import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -109,6 +111,7 @@
import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.LocaleListCompat;
import androidx.core.util.ObjectsCompat;
import androidx.core.view.KeyEventDispatcher;
import androidx.core.view.LayoutInflaterCompat;
@@ -125,6 +128,7 @@
import org.xmlpull.v1.XmlPullParser;
import java.util.List;
+import java.util.Locale;
/**
* @hide
@@ -258,9 +262,8 @@
private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED;
private int mThemeResId;
- private boolean mActivityHandlesUiMode;
- private boolean mActivityHandlesUiModeChecked;
-
+ private int mActivityHandlesConfigFlags;
+ private boolean mActivityHandlesConfigFlagsChecked;
private AutoNightModeManager mAutoTimeNightModeManager;
private AutoNightModeManager mAutoBatteryNightModeManager;
@@ -360,13 +363,22 @@
final int modeToApply = mapNightMode(baseContext, calculateNightMode());
+ if (isAutoStorageOptedIn(baseContext)) {
+ // If the developer has opted in to auto store the locales, then we use
+ // syncRequestedAndStoredLocales() to load the saved locales from storage. This is
+ // performed only during cold app start-ups because in other cases the locales can be
+ // found in the static storage.
+ syncRequestedAndStoredLocales(baseContext);
+ }
+ final LocaleListCompat localesToApply = calculateApplicationLocales(baseContext);
+
// If the base context is a ContextThemeWrapper (thus not an Application context)
// and nobody's touched its Resources yet, we can shortcut and directly apply our
// override configuration.
if (sCanApplyOverrideConfiguration
&& baseContext instanceof android.view.ContextThemeWrapper) {
- final Configuration config = createOverrideConfigurationForDayNight(
- baseContext, modeToApply, null);
+ final Configuration config = createOverrideAppConfiguration(
+ baseContext, modeToApply, localesToApply, null);
if (DEBUG) {
Log.d(TAG, String.format("Attempting to apply config to base context: %s",
config.toString()));
@@ -385,8 +397,8 @@
// Again, but using the AppCompat version of ContextThemeWrapper.
if (baseContext instanceof ContextThemeWrapper) {
- final Configuration config = createOverrideConfigurationForDayNight(
- baseContext, modeToApply, null);
+ final Configuration config = createOverrideAppConfiguration(
+ baseContext, modeToApply, localesToApply, null);
if (DEBUG) {
Log.d(TAG, String.format("Attempting to apply config to base context: %s",
config.toString()));
@@ -442,8 +454,8 @@
}
}
- final Configuration config = createOverrideConfigurationForDayNight(
- baseContext, modeToApply, configOverlay);
+ final Configuration config = createOverrideAppConfiguration(
+ baseContext, modeToApply, localesToApply, configOverlay);
if (DEBUG) {
Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
+ "applyOverrideConfiguration(). Config: %s", config.toString()));
@@ -497,9 +509,9 @@
// Dialogs, etc
mBaseContextAttached = true;
- // Our implicit call to applyDayNight() should not recreate until after the Activity is
- // created
- applyDayNight(false);
+ // Our implicit call to applyApplicationSpecificConfig() should not recreate
+ // until after the Activity is created
+ applyApplicationSpecificConfig(false);
// We lazily fetch the Window for Activities, to allow DayNight to apply in
// attachBaseContext
@@ -661,16 +673,18 @@
// inspects the last-seen configuration. Otherwise, we'll recurse back to this method.
mEffectiveConfiguration = new Configuration(mContext.getResources().getConfiguration());
- // Re-apply Day/Night with the new configuration but disable recreations. Since this
- // configuration change has only just happened we can safely just update the resources now
- applyDayNight(false);
+ // Re-apply Day/Night and locales with the new configuration but disable recreations.
+ // Since this configuration change has only just happened we can safely just update the
+ // resources now.
+ applyApplicationSpecificConfig(false);
}
@Override
public void onStart() {
// This will apply day/night if the time has changed, it will also call through to
- // setupAutoNightModeIfNeeded()
- applyDayNight();
+ // setupAutoNightModeIfNeeded(). Also, this will make sure that the latest
+ // app-specific locales are applied and activity is recreated if needed.
+ applyApplicationSpecificConfig(true);
}
@Override
@@ -2369,15 +2383,35 @@
}
@Override
+ public Context getContextForDelegate() {
+ return mContext;
+ }
+
+ @Override
public boolean applyDayNight() {
- return applyDayNight(true);
+ return applyApplicationSpecificConfig(true);
+ }
+
+ @Override
+ boolean applyAppLocales() {
+ // This method is only reached when there is an explicit call to setApplicationLocales().
+ if (isAutoStorageOptedIn(mContext)
+ && getRequestedAppLocales() != null
+ && !getRequestedAppLocales().equals(getStoredAppLocales())) {
+ // If the developer has opted in to autoStore the locales, we need to store the locales
+ // for the application here. This is done using the syncRequestedAndStoredLocales,
+ // called asynchronously on a worker thread.
+ asyncExecuteSyncRequestedAndStoredLocales(mContext);
+ }
+ return applyApplicationSpecificConfig(true);
}
@SuppressWarnings("deprecation")
- private boolean applyDayNight(final boolean allowRecreation) {
+ private boolean applyApplicationSpecificConfig(final boolean allowRecreation) {
if (mDestroyed) {
if (DEBUG) {
- Log.d(TAG, "applyDayNight. Skipping because host is destroyed");
+ Log.d(TAG, "applyApplicationSpecificConfig. Skipping because host is "
+ + "destroyed");
}
// If we're destroyed, ignore the call
return false;
@@ -2385,7 +2419,14 @@
@NightMode final int nightMode = calculateNightMode();
@ApplyableNightMode final int modeToApply = mapNightMode(mContext, nightMode);
- final boolean applied = updateForNightMode(modeToApply, allowRecreation);
+
+ LocaleListCompat localesToBeApplied = null;
+ if (Build.VERSION.SDK_INT < 33) {
+ localesToBeApplied = calculateApplicationLocales(mContext);
+ }
+
+ final boolean applied = updateAppConfiguration(modeToApply, localesToBeApplied,
+ allowRecreation);
if (nightMode == MODE_NIGHT_AUTO_TIME) {
getAutoTimeNightModeManager(mContext).setup();
@@ -2403,6 +2444,50 @@
return applied;
}
+ /**
+ * Returns the required {@link LocaleListCompat} for the current application. This method
+ * checks for requested app-specific locales and returns them after an overlay
+ * with the system locales. If requested app-specific do not exist, it returns a null.
+ */
+ @Nullable
+ LocaleListCompat calculateApplicationLocales(@NonNull Context context) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ return null;
+ }
+ LocaleListCompat requestedLocales = getRequestedAppLocales();
+ if (requestedLocales == null) {
+ return null;
+ }
+ LocaleListCompat systemLocales = getConfigurationLocales(
+ context.getApplicationContext()
+ .getResources().getConfiguration());
+
+ LocaleListCompat localesToBeApplied;
+ if (Build.VERSION.SDK_INT >= 24) {
+ // For API>=24 the application locales are applied as a localeList. The localeList
+ // to be applied is an overlay of app-specific locales and the system locales.
+ localesToBeApplied = combineLocalesIfOverlayExists(requestedLocales,
+ systemLocales);
+ } else {
+ // For API<24 the application does not have a localeList instead it has a single
+ // locale, which we have set as the locale with the highest preference i.e. the first
+ // one from the requested locales.
+ if (requestedLocales.isEmpty()) {
+ localesToBeApplied = LocaleListCompat.getEmptyLocaleList();
+ } else {
+ localesToBeApplied =
+ LocaleListCompat.forLanguageTags(requestedLocales.get(0).toString());
+ }
+ }
+
+ if (localesToBeApplied.isEmpty()) {
+ // If the localesToBeApplied is empty, it implies that there are no app-specific locales
+ // set for this application and systemLocales should be followed.
+ localesToBeApplied = systemLocales;
+ }
+ return localesToBeApplied;
+ }
+
@Override
@RequiresApi(17)
public void setLocalNightMode(@NightMode int mode) {
@@ -2461,9 +2546,38 @@
return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode();
}
+ void setConfigurationLocales(Configuration conf, @NonNull LocaleListCompat locales) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Api24Impl.setLocales(conf, locales);
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ Api17Impl.setLocale(conf, locales.get(0));
+ Api17Impl.setLayoutDirection(conf, locales.get(0));
+ } else {
+ conf.locale = locales.get(0);
+ }
+ }
+
+ LocaleListCompat getConfigurationLocales(Configuration conf) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return Api24Impl.getLocales(conf);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ return LocaleListCompat.forLanguageTags(Api21Impl.toLanguageTag(conf.locale));
+ } else {
+ return LocaleListCompat.create(conf.locale);
+ }
+ }
+
+ void setDefaultLocalesForLocaleList(LocaleListCompat locales) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Api24Impl.setDefaultLocales(locales);
+ } else {
+ Locale.setDefault(locales.get(0));
+ }
+ }
+
@NonNull
- private Configuration createOverrideConfigurationForDayNight(
- @NonNull Context context, @ApplyableNightMode final int mode,
+ private Configuration createOverrideAppConfiguration(@NonNull Context context,
+ @ApplyableNightMode int mode, @Nullable LocaleListCompat locales,
@Nullable Configuration configOverlay) {
int newNightMode;
switch (mode) {
@@ -2492,43 +2606,76 @@
overrideConf.uiMode = newNightMode
| (overrideConf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+ if (locales != null) {
+ setConfigurationLocales(overrideConf, locales);
+ }
return overrideConf;
}
/**
- * Updates the {@link Resources} configuration {@code uiMode} with the
- * chosen {@code UI_MODE_NIGHT} value.
+ * Updates the {@link Resources} configuration {@code uiMode} and {@Link LocaleList} with the
+ * chosen configuration values.
*
- * @param mode The new night mode to apply
+ * @param nightMode The new night mode to apply
+ * @param locales The new Locales to be applied
* @param allowRecreation whether to attempt activity recreate
* @return true if an action has been taken (recreation, resources updating, etc)
*/
- private boolean updateForNightMode(@ApplyableNightMode final int mode,
- final boolean allowRecreation) {
+ private boolean updateAppConfiguration(int nightMode, @Nullable LocaleListCompat
+ locales, final boolean allowRecreation) {
boolean handled = false;
final Configuration overrideConfig =
- createOverrideConfigurationForDayNight(mContext, mode, null);
+ createOverrideAppConfiguration(mContext, nightMode, locales, null);
- final boolean activityHandlingUiMode = isActivityManifestHandlingUiMode();
+ final int activityHandlingConfigChange = getActivityHandlesConfigChangesFlags();
final Configuration currentConfiguration = mEffectiveConfiguration == null
? mContext.getResources().getConfiguration() : mEffectiveConfiguration;
final int currentNightMode = currentConfiguration.uiMode
& Configuration.UI_MODE_NIGHT_MASK;
final int newNightMode = overrideConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
- if (DEBUG) {
- Log.d(TAG, String.format(
- "updateForNightMode [allowRecreation:%s, currentNightMode:%d, "
- + "newNightMode:%d, activityHandlingUiMode:%s, baseContextAttached:%s, "
- + "created:%s, canReturnDifferentContext:%s, host:%s]",
- allowRecreation, currentNightMode, newNightMode, activityHandlingUiMode,
- mBaseContextAttached, mCreated, sCanReturnDifferentContext, mHost));
+ final LocaleListCompat currentLocales = getConfigurationLocales(currentConfiguration);
+ final LocaleListCompat newLocales;
+ if (locales == null) {
+ newLocales = null;
+ } else {
+ newLocales = getConfigurationLocales(overrideConfig);
}
- if (currentNightMode != newNightMode
+ // Bitmask representing if there is a change in nightMode or Locales, mapped by bits
+ // ActivityInfo.CONFIG_UI_MODE and ActivityInfo.CONFIG_LOCALE respectively.
+ int configChanges = 0;
+ if (currentNightMode != newNightMode) {
+ configChanges |= ActivityInfo.CONFIG_UI_MODE;
+ }
+ if (newLocales != null && !currentLocales.equals(newLocales)) {
+ configChanges |= ActivityInfo.CONFIG_LOCALE;
+ if (Build.VERSION.SDK_INT >= 17) {
+ configChanges |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "updateAppConfiguration [allowRecreation:%s, "
+ + "currentNightMode:%s, newNightMode:%s, currentLocales:%s, "
+ + "newLocales:%s, activityHandlingNightModeChanges:%s, "
+ + "activityHandlingLocalesChanges:%s, "
+ + "activityHandlingLayoutDirectionChanges:%s, "
+ + "baseContextAttached:%s, "
+ + "created:%s, canReturnDifferentContext:%s, host:%s]",
+ allowRecreation, currentNightMode, newNightMode,
+ currentLocales,
+ newLocales,
+ ((activityHandlingConfigChange & ActivityInfo.CONFIG_UI_MODE) != 0),
+ ((activityHandlingConfigChange & ActivityInfo.CONFIG_LOCALE) != 0),
+ ((activityHandlingConfigChange & ActivityInfo.CONFIG_LAYOUT_DIRECTION) != 0),
+ mBaseContextAttached, mCreated,
+ sCanReturnDifferentContext, mHost));
+ }
+ if ((~activityHandlingConfigChange & configChanges) != 0
&& allowRecreation
- && !activityHandlingUiMode
&& mBaseContextAttached
&& (sCanReturnDifferentContext || mCreated)
&& mHost instanceof Activity
@@ -2537,41 +2684,56 @@
// attachBaseContext() + createConfigurationContext() code path.
// Else, we need to use updateConfiguration() before we're 'created' (below)
if (DEBUG) {
- Log.d(TAG, "updateForNightMode attempting to recreate Activity: " + mHost);
+ Log.d(TAG, "updateAppConfiguration attempting to recreate Activity: "
+ + mHost);
}
ActivityCompat.recreate((Activity) mHost);
handled = true;
} else if (DEBUG) {
- Log.d(TAG, "updateForNightMode not recreating Activity: " + mHost);
+ Log.d(TAG, "updateAppConfiguration not recreating Activity: " + mHost);
}
- if (!handled && currentNightMode != newNightMode) {
+ if (!handled && (configChanges != 0)) {
// Else we need to use the updateConfiguration path
if (DEBUG) {
- Log.d(TAG, "updateForNightMode. Updating resources config on host: " + mHost);
+ Log.d(TAG, "updateAppConfiguration. Updating resources config on host: "
+ + mHost);
}
- updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode, null);
+ // If all the configurations that need to be altered are handled by the activity,
+ // only then callOnConfigChange is set to true.
+ updateResourcesConfiguration(newNightMode, newLocales,
+ /* callOnConfigChange = */(configChanges & activityHandlingConfigChange)
+ == configChanges, null);
+
handled = true;
}
if (DEBUG && !handled) {
- Log.d(TAG, "updateForNightMode. Skipping. Night mode: " + mode + " for host:" + mHost);
+ Log.d(TAG,
+ "updateAppConfiguration. Skipping. nightMode: " + nightMode + " and "
+ + "locales: " + locales + " for host:" + mHost);
}
- // Notify the activity of the night mode. We only notify if we handled the change,
- // or the Activity is set to handle uiMode changes
if (handled && mHost instanceof AppCompatActivity) {
- ((AppCompatActivity) mHost).onNightModeChanged(mode);
+ if ((configChanges & ActivityInfo.CONFIG_UI_MODE) != 0) {
+ ((AppCompatActivity) mHost).onNightModeChanged(nightMode);
+ }
+ if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
+ ((AppCompatActivity) mHost).onLocalesChanged(locales);
+ }
}
+ if (handled && newLocales != null) {
+ setDefaultLocalesForLocaleList(newLocales);
+ }
return handled;
}
- private void updateResourcesConfigurationForNightMode(
- final int uiModeNightModeValue, final boolean callOnConfigChange,
+ private void updateResourcesConfiguration(int uiModeNightModeValue,
+ @Nullable final LocaleListCompat locales, final boolean callOnConfigChange,
@Nullable Configuration configOverlay) {
- // If the Activity is not set to handle uiMode config changes we will
- // update the Resources with a new Configuration with an updated UI Mode
+ // If the Activity is not set to handle config changes we will
+ // update the Resources with a new Configuration with updated nightMode and locales.
final Resources res = mContext.getResources();
final Configuration conf = new Configuration(res.getConfiguration());
if (configOverlay != null) {
@@ -2579,6 +2741,9 @@
}
conf.uiMode = uiModeNightModeValue
| (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+ if (locales != null) {
+ setConfigurationLocales(conf, locales);
+ }
res.updateConfiguration(conf, null);
// We may need to flush the Resources' drawable cache due to framework bugs.
@@ -2602,19 +2767,23 @@
}
if (callOnConfigChange && mHost instanceof Activity) {
- final Activity activity = (Activity) mHost;
- if (activity instanceof LifecycleOwner) {
- // If the Activity is a LifecyleOwner, check that it is after onCreate() and
- // before onDestroy(), which includes STOPPED.
- Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
- if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) {
- activity.onConfigurationChanged(conf);
- }
- } else {
- // Otherwise, we'll fallback to our internal created and destroyed flags.
- if (mCreated && !mDestroyed) {
- activity.onConfigurationChanged(conf);
- }
+ updateActivityConfiguration(conf);
+ }
+ }
+
+ private void updateActivityConfiguration(Configuration conf) {
+ final Activity activity = (Activity) mHost;
+ if (activity instanceof LifecycleOwner) {
+ // If the Activity is a LifecyleOwner, check that it is after onCreate() and
+ // before onDestroy(), which includes STOPPED.
+ Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
+ if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) {
+ activity.onConfigurationChanged(conf);
+ }
+ } else {
+ // Otherwise, we'll fallback to our internal created and destroyed flags.
+ if (mCreated && !mDestroyed) {
+ activity.onConfigurationChanged(conf);
}
}
}
@@ -2644,13 +2813,14 @@
return mAutoBatteryNightModeManager;
}
- private boolean isActivityManifestHandlingUiMode() {
- if (!mActivityHandlesUiModeChecked && mHost instanceof Activity) {
+ private int getActivityHandlesConfigChangesFlags() {
+ if (!mActivityHandlesConfigFlagsChecked
+ && mHost instanceof Activity) {
final PackageManager pm = mContext.getPackageManager();
if (pm == null) {
- // If we don't have a PackageManager, return false. Don't set
+ // If we don't have a PackageManager, return 0. Don't set
// the checked flag though so we still check again later
- return false;
+ return 0;
}
try {
int flags = 0;
@@ -2667,19 +2837,19 @@
}
final ActivityInfo info = pm.getActivityInfo(
new ComponentName(mContext, mHost.getClass()), flags);
- mActivityHandlesUiMode = info != null
- && (info.configChanges & ActivityInfo.CONFIG_UI_MODE) != 0;
+ if (info != null) {
+ mActivityHandlesConfigFlags = info.configChanges;
+ }
} catch (PackageManager.NameNotFoundException e) {
// This shouldn't happen but let's not crash because of it, we'll just log and
// return false (since most apps won't be handling it)
Log.d(TAG, "Exception while getting ActivityInfo", e);
- mActivityHandlesUiMode = false;
+ mActivityHandlesConfigFlags = 0;
}
}
// Flip the checked flag so we don't check again
- mActivityHandlesUiModeChecked = true;
-
- return mActivityHandlesUiMode;
+ mActivityHandlesConfigFlagsChecked = true;
+ return mActivityHandlesConfigFlags;
}
/**
@@ -3603,6 +3773,16 @@
@NonNull Configuration overrideConfiguration) {
return context.createConfigurationContext(overrideConfiguration);
}
+
+ @DoNotInline
+ static void setLayoutDirection(Configuration configuration, Locale loc) {
+ configuration.setLayoutDirection(loc);
+ }
+
+ @DoNotInline
+ static void setLocale(Configuration configuration, Locale loc) {
+ configuration.setLocale(loc);
+ }
}
@RequiresApi(21)
@@ -3612,12 +3792,20 @@
static boolean isPowerSaveMode(PowerManager powerManager) {
return powerManager.isPowerSaveMode();
}
+
+ @DoNotInline
+ static String toLanguageTag(Locale locale) {
+ return locale.toLanguageTag();
+ }
}
@RequiresApi(24)
static class Api24Impl {
private Api24Impl() { }
+ // Most methods of LocaleListCompat requires a minimum API of 24 to be used and these are
+ // the helper implementations of those methods, used to indirectly invoke them in our code.
+ @DoNotInline
static void generateConfigDelta_locale(@NonNull Configuration base,
@NonNull Configuration change, @NonNull Configuration delta) {
final LocaleList baseLocales = base.getLocales();
@@ -3627,6 +3815,21 @@
delta.locale = change.locale;
}
}
+
+ @DoNotInline
+ static LocaleListCompat getLocales(Configuration configuration) {
+ return LocaleListCompat.forLanguageTags(configuration.getLocales().toLanguageTags());
+ }
+
+ @DoNotInline
+ static void setLocales(Configuration configuration, LocaleListCompat locales) {
+ configuration.setLocales(LocaleList.forLanguageTags(locales.toLanguageTags()));
+ }
+
+ @DoNotInline
+ public static void setDefaultLocales(LocaleListCompat locales) {
+ LocaleList.setDefault(LocaleList.forLanguageTags(locales.toLanguageTags()));
+ }
}
@RequiresApi(26)
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java
new file mode 100644
index 0000000..3fe13bc
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 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 androidx.appcompat.app;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.LocaleListCompat;
+
+/**
+ * A placeholder service to avoid adding application-level metadata. The service
+ * is only used to expose metadata defined in the library's manifest. It is
+ * never invoked.
+ *
+ * <p>This metadata boolean collected from the key "autoStoreLocales" is used as an opt-in from
+ * the developers to automatically store locales provided by them through the API
+ * {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}.</p>
+ */
+public final class AppLocalesMetadataHolderService extends Service {
+ public AppLocalesMetadataHolderService() {}
+
+ @NonNull
+ @Override
+ public IBinder onBind(@SuppressWarnings("InvalidNullability") @NonNull Intent intent) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the {@link ServiceInfo} for the declared {@link AppLocalesMetadataHolderService}.
+ *
+ * <p>This serviceInfo contains the attribute "autoStoreLocales", its value being a boolean
+ * that informs us if the developer wants us to handle the storage of locales or not.</p>
+ */
+ @NonNull
+ @SuppressWarnings("deprecation") // GET_DISABLED_COMPONENTS, getServiceInfo
+ public static ServiceInfo getServiceInfo(@NonNull Context context) throws
+ PackageManager.NameNotFoundException {
+ int flags = PackageManager.GET_META_DATA;
+ // The service is marked as disabled so we need to include the following flags.
+ if (Build.VERSION.SDK_INT >= 24) {
+ flags |= Api24Impl.getDisabledComponentFlag();
+ } else {
+ flags |= PackageManager.GET_DISABLED_COMPONENTS;
+ }
+
+ return context.getPackageManager().getServiceInfo(
+ new ComponentName(context, AppLocalesMetadataHolderService.class), flags);
+ }
+
+ @RequiresApi(24)
+ private static class Api24Impl {
+ @DoNotInline
+ static int getDisabledComponentFlag() {
+ return PackageManager.MATCH_DISABLED_COMPONENTS;
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
new file mode 100644
index 0000000..d30f599
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2022 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 androidx.appcompat.app;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+
+import static androidx.appcompat.app.AppCompatDelegate.getApplicationLocales;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.LocaleListCompat;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class to manage storage of locales in app's persistent files.
+ */
+class AppLocalesStorageHelper {
+ static final String APPLICATION_LOCALES_RECORD_FILE =
+ "androidx.appcompat.app.AppCompatDelegate.application_locales_record_file";
+ static final String LOCALE_RECORD_ATTRIBUTE_TAG = "application_locales";
+ static final String LOCALE_RECORD_FILE_TAG = "locales";
+ static final String APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME = "androidx.appcompat.app"
+ + ".AppLocalesMetadataHolderService";
+ static final String TAG = "AppLocalesStorageHelper";
+
+ private AppLocalesStorageHelper() {}
+
+ /**
+ * Returns app locales after reading from storage, fetched using the application context.
+ */
+ @NonNull
+ static String readLocales(@NonNull Context context) {
+ String appLocales = "";
+
+ FileInputStream fis;
+ try {
+ fis = context.openFileInput(APPLICATION_LOCALES_RECORD_FILE);
+ } catch (FileNotFoundException fnfe) {
+ Log.w(TAG, "Reading app Locales : Locales record file not found: "
+ + APPLICATION_LOCALES_RECORD_FILE);
+ return appLocales;
+ }
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, "UTF-8");
+ int type;
+ int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ String tagName = parser.getName();
+ if (tagName.equals(LOCALE_RECORD_FILE_TAG)) {
+ appLocales = parser.getAttributeValue(/*namespace= */ null,
+ LOCALE_RECORD_ATTRIBUTE_TAG);
+ break;
+ }
+ }
+ } catch (XmlPullParserException | IOException e) {
+ Log.w(TAG,
+ "Reading app Locales : Unable to parse through file :"
+ + APPLICATION_LOCALES_RECORD_FILE);
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ /* ignore */
+ }
+ }
+ }
+
+ if (!appLocales.isEmpty()) {
+ Log.d(TAG,
+ "Reading app Locales : Locales read from file: "
+ + APPLICATION_LOCALES_RECORD_FILE + " ," + " appLocales: "
+ + appLocales);
+ } else {
+ context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+ }
+ return appLocales;
+ }
+
+ /**
+ * Stores the provided locales in internal app file, using the application context.
+ */
+ static void persistLocales(@NonNull Context context, @NonNull String locales) {
+ if (locales.equals("")) {
+ context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+ return;
+ }
+
+ FileOutputStream fos;
+ try {
+ fos = context.openFileOutput(APPLICATION_LOCALES_RECORD_FILE, Context.MODE_PRIVATE);
+ } catch (FileNotFoundException fnfe) {
+ Log.w(TAG, String.format("Storing App Locales : FileNotFoundException: Cannot open "
+ + "file %s for writing ", APPLICATION_LOCALES_RECORD_FILE));
+ return;
+ }
+ XmlSerializer serializer = Xml.newSerializer();
+ try {
+ serializer.setOutput(fos, /* encoding= */ null);
+ serializer.startDocument("UTF-8", true);
+ serializer.startTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+ serializer.attribute(/* namespace= */ null, LOCALE_RECORD_ATTRIBUTE_TAG, locales);
+ serializer.endTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+ serializer.endDocument();
+ Log.d(TAG, "Storing App Locales : app-locales: "
+ + locales + " persisted successfully.");
+ } catch (Exception e) {
+ Log.w(TAG, "Storing App Locales : Failed to persist app-locales: "
+ + locales, e);
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ /* ignore */
+ }
+ }
+ }
+ }
+
+ /**
+ * Syncs app-specific locales from androidX to framework. This is used to maintain a smooth
+ * transition for a device that updates from pre-T API versions to T.
+ *
+ * <p><b>NOTE:</b> This should only be called when auto-storage is opted-in. This method
+ * uses the meta-data service provided during the opt-in and hence if the service is not found
+ * this method will throw an error.</p>
+ */
+ static void syncLocalesToFramework(Context context) {
+ ComponentName app_locales_component = new ComponentName(
+ context, APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME);
+
+ if (context.getPackageManager().getComponentEnabledSetting(app_locales_component)
+ != COMPONENT_ENABLED_STATE_ENABLED) {
+ AppCompatDelegate.setAppContext(context);
+ // ComponentEnabledSetting for the app component app_locales_component is used as a
+ // marker to represent that the locales has been synced from AndroidX to framework
+ // If this marker is found in ENABLED state then we do not need to sync again.
+ if (getApplicationLocales().isEmpty()) {
+ // We check if some locales are applied by the framework or not (this is done to
+ // ensure that we don't overwrite newer locales set by the framework). If no
+ // app-locales are found then we need to sync the app-specific locales from androidX
+ // to framework.
+
+ String appLocales = readLocales(context);
+ // if locales are present in storage, call the setApplicationLocales() API. As the
+ // API version is >= 33, this call will be directed to the framework API and the
+ // locales will be persisted there.
+ AppCompatDelegate.setApplicationLocales(
+ LocaleListCompat.forLanguageTags(appLocales));
+ }
+ // setting ComponentEnabledSetting for app component using
+ // AppLocalesMetadataHolderService (used only for locales, thus minimizing
+ // the chances of conflicts). Setting it as ENABLED marks the success of app-locales
+ // sync from AndroidX to framework.
+ // Flag DONT_KILL_APP indicates that you don't want to kill the app containing the
+ // component.
+ context.getPackageManager().setComponentEnabledSetting(app_locales_component,
+ COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ DONT_KILL_APP);
+ }
+ }
+
+ /**
+ * Implementation of {@link java.util.concurrent.Executor} that executes each runnable on a
+ * new thread.
+ */
+ static class ThreadPerTaskExecutor implements Executor {
+ @Override
+ public void execute(Runnable r) {
+ new Thread(r).start();
+ }
+ }
+
+ /**
+ * Implementation of {@link java.util.concurrent.Executor} that executes runnables serially
+ * by synchronizing the {@link Executor#execute(Runnable)} method and maintaining a tasks
+ * queue.
+ */
+ static class SerialExecutor implements Executor {
+ private final Object mLock = new Object();
+ final Queue<Runnable> mTasks = new ArrayDeque<>();
+ final Executor mExecutor;
+ Runnable mActive;
+
+ SerialExecutor(Executor executor) {
+ this.mExecutor = executor;
+ }
+
+ @Override
+ public void execute(final Runnable r) {
+ synchronized (mLock) {
+ mTasks.add(() -> {
+ try {
+ r.run();
+ } finally {
+ scheduleNext();
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+ }
+
+ protected void scheduleNext() {
+ synchronized (mLock) {
+ if ((mActive = mTasks.poll()) != null) {
+ mExecutor.execute(mActive);
+ }
+ }
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java
new file mode 100644
index 0000000..9684b92
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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 androidx.appcompat.app;
+
+import android.os.LocaleList;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Static utilities to overlay locales on top of another LocaleListCompat.
+ *
+ * <p>This is used to overlay application-specific locales on top of
+ * system locales.</p>
+ */
+@RequiresApi(24)
+final class LocaleOverlayHelper {
+
+ private LocaleOverlayHelper() {}
+
+ /**
+ * Combines the overlay locales and base locales.
+ *
+ * @return the combined {@link LocaleListCompat} if the overlay locales is not empty/null else
+ * returns an empty LocaleListCompat.
+ */
+ static LocaleListCompat combineLocalesIfOverlayExists(LocaleListCompat overlayLocales,
+ LocaleListCompat baseLocales) {
+ if (overlayLocales == null || overlayLocales.isEmpty()) {
+ return LocaleListCompat.getEmptyLocaleList();
+ }
+ return combineLocales(overlayLocales, baseLocales);
+ }
+
+ static LocaleListCompat combineLocalesIfOverlayExists(LocaleList overlayLocales,
+ LocaleList baseLocales) {
+ if (overlayLocales == null || overlayLocales.isEmpty()) {
+ return LocaleListCompat.getEmptyLocaleList();
+ }
+ return combineLocales(LocaleListCompat.wrap(overlayLocales),
+ LocaleListCompat.wrap(baseLocales));
+ }
+
+ /**
+ * Creates a combined {@link LocaleListCompat} by placing overlay locales before base
+ * locales and dropping duplicates from the base locales.
+ */
+ private static LocaleListCompat combineLocales(LocaleListCompat overlayLocales,
+ LocaleListCompat baseLocales) {
+ // using LinkedHashSet to drop duplicates.
+ Set<Locale> combinedLocales = new LinkedHashSet<>();
+ for (int i = 0; i < overlayLocales.size() + baseLocales.size(); i++) {
+ Locale currLocale;
+ if (i < overlayLocales.size()) {
+ currLocale = overlayLocales.get(i);
+ } else {
+ currLocale = baseLocales.get(i - overlayLocales.size());
+ }
+ if (currLocale != null) {
+ combinedLocales.add(currLocale);
+ }
+ }
+ return LocaleListCompat.create(combinedLocales.toArray(
+ new Locale[combinedLocales.size()]));
+ }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
index 89e3bb9..34a8e20 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
@@ -683,6 +683,7 @@
*
* @return Whether loading was performed.
*/
+ @SuppressWarnings("deprecation")
private boolean loadActivitiesIfNeeded() {
if (mReloadActivities && mIntent != null) {
mReloadActivities = false;
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 6e9c5ba..6cc5e8b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -59,6 +59,7 @@
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.view.menu.ShowableListMenu;
+import androidx.core.util.ObjectsCompat;
import androidx.core.view.TintableBackgroundView;
import androidx.core.view.ViewCompat;
import androidx.resourceinspection.annotation.AppCompatShadowedAttributes;
@@ -1129,7 +1130,7 @@
@NonNull android.widget.ThemedSpinnerAdapter themedSpinnerAdapter,
@Nullable Resources.Theme theme
) {
- if (themedSpinnerAdapter.getDropDownViewTheme() != theme) {
+ if (!ObjectsCompat.equals(themedSpinnerAdapter.getDropDownViewTheme(), theme)) {
themedSpinnerAdapter.setDropDownViewTheme(theme);
}
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
index ff70a9c..561672f 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
@@ -23,6 +23,7 @@
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -33,10 +34,12 @@
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.appcompat.R;
import androidx.appcompat.graphics.drawable.DrawableWrapper;
import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.os.BuildCompat;
import androidx.core.view.ViewPropertyAnimatorCompat;
import androidx.core.widget.ListViewAutoScrollHelper;
@@ -62,8 +65,6 @@
private int mMotionPosition;
- private Field mIsChildViewEnabled;
-
private GateKeeperDrawable mSelector;
/*
@@ -126,15 +127,25 @@
super(context, null, R.attr.dropDownListViewStyle);
mHijackFocus = hijackFocus;
setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+ }
- try {
- mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
- mIsChildViewEnabled.setAccessible(true);
- } catch (NoSuchFieldException e) {
- e.printStackTrace();
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ private boolean superIsSelectedChildViewEnabled() {
+ if (BuildCompat.isAtLeastT()) {
+ return Api33Impl.isSelectedChildViewEnabled(this);
+ } else {
+ return PreApi33Impl.isSelectedChildViewEnabled(this);
}
}
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ private void superSetSelectedChildViewEnabled(boolean enabled) {
+ if (BuildCompat.isAtLeastT()) {
+ Api33Impl.setSelectedChildViewEnabled(this, enabled);
+ } else {
+ PreApi33Impl.setSelectedChildViewEnabled(this, enabled);
+ }
+ }
@Override
public boolean isInTouchMode() {
@@ -624,18 +635,14 @@
selectorRect.right += mSelectionRightPadding;
selectorRect.bottom += mSelectionBottomPadding;
- try {
- // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
- // modify its value
- final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
- if (sel.isEnabled() != isChildViewEnabled) {
- mIsChildViewEnabled.set(this, !isChildViewEnabled);
- if (position != INVALID_POSITION) {
- refreshDrawableState();
- }
+ // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
+ // modify its value
+ final boolean isChildViewEnabled = superIsSelectedChildViewEnabled();
+ if (sel.isEnabled() != isChildViewEnabled) {
+ superSetSelectedChildViewEnabled(!isChildViewEnabled);
+ if (position != INVALID_POSITION) {
+ refreshDrawableState();
}
- } catch (IllegalAccessException e) {
- e.printStackTrace();
}
}
@@ -803,4 +810,66 @@
view.drawableHotspotChanged(x, y);
}
}
+
+ // TODO(b/221852137): Use @DeprecatedSinceApi(33).
+ @SuppressWarnings({"JavaReflectionMemberAccess", "CatchAndPrintStackTrace"})
+ static class PreApi33Impl {
+ private static final Field sIsChildViewEnabled;
+
+ static {
+ Field isChildViewEnabled = null;
+
+ try {
+ isChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
+ isChildViewEnabled.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+
+ sIsChildViewEnabled = isChildViewEnabled;
+ }
+
+ private PreApi33Impl() {
+ // This class is not instantiable.
+ }
+
+ static boolean isSelectedChildViewEnabled(AbsListView view) {
+ if (sIsChildViewEnabled != null) {
+ try {
+ return sIsChildViewEnabled.getBoolean(view);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return false;
+ }
+
+ static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
+ if (sIsChildViewEnabled != null) {
+ try {
+ sIsChildViewEnabled.set(view, enabled);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ static class Api33Impl {
+ private Api33Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static boolean isSelectedChildViewEnabled(AbsListView view) {
+ return view.isSelectedChildViewEnabled();
+ }
+
+ @DoNotInline
+ static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
+ view.setSelectedChildViewEnabled(enabled);
+ }
+ }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
index 072c94a..3bd4492 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
@@ -900,6 +900,7 @@
updateSubmitArea();
}
+ @SuppressWarnings("deprecation")
private boolean hasVoiceSearch() {
if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
Intent testIntent = null;
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
index 173e289..77e9dae 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
@@ -123,7 +123,7 @@
public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
if (theme == null) {
mDropDownInflater = null;
- } else if (theme == mContext.getTheme()) {
+ } else if (theme.equals(mContext.getTheme())) {
mDropDownInflater = mInflater;
} else {
final Context context = new ContextThemeWrapper(mContext, theme);
diff --git a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
index 7aba8a7..2628b2e 100644
--- a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
+++ b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
@@ -93,7 +93,7 @@
PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(notes)
.build();
return Futures.transformAsync(mAppSearchSessionFuture,
- session -> session.put(request), mExecutor);
+ session -> session.putAsync(request), mExecutor);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
@@ -112,7 +112,7 @@
}
private ListenableFuture<AppSearchSession> createLocalSession() {
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(mContext, DB_NAME)
.build()
);
@@ -122,7 +122,7 @@
SetSchemaRequest request =
new SetSchemaRequest.Builder().setForceOverride(FORCE_OVERRIDE).build();
return Futures.transformAsync(mAppSearchSessionFuture,
- session -> session.setSchema(request),
+ session -> session.setSchemaAsync(request),
mExecutor);
}
@@ -131,7 +131,7 @@
SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
.build();
return Futures.transformAsync(mAppSearchSessionFuture,
- session -> session.setSchema(request), mExecutor);
+ session -> session.setSchemaAsync(request), mExecutor);
} catch (AppSearchException e) {
return Futures.immediateFailedFuture(e);
}
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
index 720d679..fce8a0d 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
@@ -81,7 +81,7 @@
* storage as the storage type for debugging.
*/
@NonNull
- public static ListenableFuture<DebugAppSearchManager> create(
+ public static ListenableFuture<DebugAppSearchManager> createAsync(
@NonNull Context context,
@NonNull ExecutorService executor, @NonNull String databaseName,
@AppSearchDebugActivity.StorageType int storageType) throws AppSearchException {
@@ -98,14 +98,13 @@
case AppSearchDebugActivity.STORAGE_TYPE_LOCAL:
debugAppSearchManagerListenableFuture =
Futures.transform(
- debugAppSearchManager.initializeLocalStorage(databaseName),
+ debugAppSearchManager.initializeLocalStorageAsync(databaseName),
unused -> debugAppSearchManager, executor);
break;
case AppSearchDebugActivity.STORAGE_TYPE_PLATFORM:
if (Build.VERSION.SDK_INT >= 31) {
- debugAppSearchManagerListenableFuture =
- Futures.transform(
- debugAppSearchManager.initializePlatformStorage(databaseName),
+ debugAppSearchManagerListenableFuture = Futures.transform(
+ debugAppSearchManager.initializePlatformStorageAsync(databaseName),
unused -> debugAppSearchManager, executor);
} else {
throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
@@ -130,7 +129,7 @@
* {@link #getNextPage} to retrieve the {@link GenericDocument} objects for each page.
*/
@NonNull
- public ListenableFuture<SearchResults> getAllDocumentsSearchResults() {
+ public ListenableFuture<SearchResults> getAllDocumentsSearchResultsAsync() {
SearchSpec searchSpec = new SearchSpec.Builder()
.setResultCountPerPage(PAGE_SIZE)
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -151,10 +150,11 @@
* {@link GenericDocument} objects.
*/
@NonNull
- public ListenableFuture<List<GenericDocument>> getNextPage(@NonNull SearchResults results) {
+ public ListenableFuture<List<GenericDocument>> getNextPageAsync(
+ @NonNull SearchResults results) {
Preconditions.checkNotNull(results);
- return Futures.transform(results.getNextPage(),
+ return Futures.transform(results.getNextPageAsync(),
DebugAppSearchManager::convertResultsToGenericDocuments, mExecutor);
}
@@ -162,7 +162,7 @@
* Gets a document from the AppSearch database by namespace and ID.
*/
@NonNull
- public ListenableFuture<GenericDocument> getDocument(@NonNull String namespace,
+ public ListenableFuture<GenericDocument> getDocumentAsync(@NonNull String namespace,
@NonNull String id) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(namespace);
@@ -170,7 +170,7 @@
new GetByDocumentIdRequest.Builder(namespace).addIds(id).build();
return Futures.transformAsync(mAppSearchSessionFuture,
- session -> Futures.transform(session.getByDocumentId(request),
+ session -> Futures.transform(session.getByDocumentIdAsync(request),
response -> response.getSuccesses().get(id), mExecutor), mExecutor);
}
@@ -178,9 +178,9 @@
* Gets the schema of the AppSearch database.
*/
@NonNull
- public ListenableFuture<GetSchemaResponse> getSchema() {
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
return Futures.transformAsync(mAppSearchSessionFuture,
- session -> session.getSchema(), mExecutor);
+ session -> session.getSchemaAsync(), mExecutor);
}
/**
@@ -195,9 +195,9 @@
}
@NonNull
- private ListenableFuture<AppSearchSession> initializeLocalStorage(
+ private ListenableFuture<AppSearchSession> initializeLocalStorageAsync(
@NonNull String databaseName) {
- mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
+ mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(mContext, databaseName)
.build())
);
@@ -206,9 +206,9 @@
@NonNull
@RequiresApi(Build.VERSION_CODES.S)
- private ListenableFuture<AppSearchSession> initializePlatformStorage(
+ private ListenableFuture<AppSearchSession> initializePlatformStorageAsync(
@NonNull String databaseName) {
- mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSession(
+ mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(mContext, databaseName)
.build())
);
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
index 5a23cb0..6425c63 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
@@ -76,7 +76,7 @@
*/
@NonNull
public LiveData<SearchResults> getAllDocumentsSearchResults() {
- Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResults(),
+ Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResultsAsync(),
new FutureCallback<SearchResults>() {
@Override
public void onSuccess(SearchResults result) {
@@ -107,7 +107,7 @@
@NonNull
public LiveData<List<GenericDocument>> addAdditionalResultsPage(
@NonNull SearchResults results) {
- Futures.addCallback(mDebugAppSearchManager.getNextPage(results),
+ Futures.addCallback(mDebugAppSearchManager.getNextPageAsync(results),
new FutureCallback<List<GenericDocument>>() {
@Override
public void onSuccess(List<GenericDocument> result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
index a8c03dc..41d2ed1 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
@@ -61,7 +61,7 @@
*/
@NonNull
public LiveData<GenericDocument> getDocument(@NonNull String namespace, @NonNull String id) {
- Futures.addCallback(mDebugAppSearchManager.getDocument(namespace, id),
+ Futures.addCallback(mDebugAppSearchManager.getDocumentAsync(namespace, id),
new FutureCallback<GenericDocument>() {
@Override
public void onSuccess(GenericDocument result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
index 16e0b66..70cc1ee 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
@@ -90,7 +90,7 @@
*/
@NonNull
private LiveData<GetSchemaResponse> getSchema() {
- Futures.addCallback(mDebugAppSearchManager.getSchema(),
+ Futures.addCallback(mDebugAppSearchManager.getSchemaAsync(),
new FutureCallback<GetSchemaResponse>() {
@Override
public void onSuccess(GetSchemaResponse result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
index 20ea01f..3174e2d 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
@@ -97,7 +97,7 @@
@StorageType int storageType =
getIntent().getExtras().getInt(STORAGE_TYPE_INTENT_KEY);
try {
- mDebugAppSearchManager = DebugAppSearchManager.create(
+ mDebugAppSearchManager = DebugAppSearchManager.createAsync(
getApplicationContext(), mBackgroundExecutor, mDbName, storageType);
} catch (AppSearchException e) {
Toast.makeText(getApplicationContext(),
diff --git a/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
index 7636104..85eb735 100644
--- a/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
+++ b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
@@ -43,7 +43,7 @@
@Before
public fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
- session = LocalStorage.createSearchSession(
+ session = LocalStorage.createSearchSessionAsync(
LocalStorage.SearchContext.Builder(context, DB_NAME).build()
).get()
@@ -59,7 +59,7 @@
}
private fun cleanup() {
- session.setSchema(SetSchemaRequest.Builder().setForceOverride(true).build()).get()
+ session.setSchemaAsync(SetSchemaRequest.Builder().setForceOverride(true).build()).get()
}
@Document
@@ -250,7 +250,7 @@
@Test
fun testAnnotationProcessor() {
- session.setSchema(
+ session.setSchemaAsync(
SetSchemaRequest.Builder()
.addDocumentClasses(Card::class.java, Gift::class.java).build()
).get()
@@ -260,7 +260,7 @@
// Index the Gift document and query it.
checkIsBatchResultSuccess(
- session.put(
+ session.putAsync(
PutDocumentsRequest.Builder().addDocuments(inputDocument).build()
)
)
diff --git a/appsearch/appsearch-local-storage/api/current.txt b/appsearch/appsearch-local-storage/api/current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/current.txt
+++ b/appsearch/appsearch-local-storage/api/current.txt
@@ -2,7 +2,8 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
@@ -2,7 +2,8 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/api/restricted_current.txt b/appsearch/appsearch-local-storage/api/restricted_current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-local-storage/api/restricted_current.txt
@@ -2,7 +2,8 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/build.gradle b/appsearch/appsearch-local-storage/build.gradle
index 268af48..d723e00 100644
--- a/appsearch/appsearch-local-storage/build.gradle
+++ b/appsearch/appsearch-local-storage/build.gradle
@@ -44,12 +44,11 @@
defaultConfig {
externalNativeBuild {
cmake {
- cppFlags "-std=c++17"
arguments "-DCMAKE_VERBOSE_MAKEFILE=ON"
targets "icing"
}
}
- multiDexEnabled true
+ multiDexEnabled true
}
externalNativeBuild {
cmake {
@@ -72,9 +71,9 @@
api("androidx.annotation:annotation:1.1.0")
implementation(project(":appsearch:appsearch"))
+ implementation('androidx.collection:collection:1.2.0')
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.core:core:1.2.0")
- implementation('androidx.collection:collection:1.0.0')
androidTestImplementation project(':appsearch:appsearch-test-util')
androidTestImplementation(libs.multidex)
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 961543b..22128b0 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -25,20 +25,28 @@
import static org.junit.Assert.assertThrows;
import android.content.Context;
-import android.os.Process;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.stats.InitializeStats;
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.appsearch.testutil.TestObserverCallback;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.test.core.app.ApplicationProvider;
@@ -58,6 +66,7 @@
import com.google.android.icing.proto.TermMatchType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
@@ -72,6 +81,7 @@
import java.util.Map;
import java.util.Set;
+@SuppressWarnings("GuardedBy")
public class AppSearchImplTest {
/**
* Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
@@ -80,6 +90,12 @@
@Rule
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
private File mAppSearchDir;
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ // The caller access for this package
+ private final CallerAccess mSelfCallerAccess = new CallerAccess(mContext.getPackageName());
+
private AppSearchImpl mAppSearchImpl;
@Before
@@ -88,7 +104,9 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
}
@After
@@ -366,9 +384,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -409,17 +425,14 @@
@Test
public void testReset() throws Exception {
// Insert schema
- Context context = ApplicationProvider.getApplicationContext();
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build());
mAppSearchImpl.setSchema(
- context.getPackageName(),
+ mContext.getPackageName(),
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -428,7 +441,7 @@
GenericDocument validDoc =
new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
mAppSearchImpl.putDocument(
- context.getPackageName(),
+ mContext.getPackageName(),
"database1",
validDoc,
/*logger=*/null);
@@ -438,10 +451,7 @@
SearchResultPage results = mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- context.getPackageName(),
- /*visibilityStore=*/ null,
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mSelfCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).hasSize(1);
assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -450,7 +460,7 @@
DocumentProto invalidDoc = DocumentProto.newBuilder()
.setNamespace("invalidNamespace")
.setUri("id2")
- .setSchema(context.getPackageName() + "$database1/Type1")
+ .setSchema(mContext.getPackageName() + "$database1/Type1")
.build();
AppSearchException e = assertThrows(
AppSearchException.class,
@@ -466,7 +476,8 @@
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
- mAppSearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+ mAppSearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Check recovery state
InitializeStats initStats = initStatsBuilder.build();
@@ -485,33 +496,32 @@
assertThat(initStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
// Make sure all our data is gone
- assertThat(mAppSearchImpl.getSchema(context.getPackageName(), "database1").getSchemas())
+ assertThat(mAppSearchImpl.getSchema(
+ /*packageName=*/mContext.getPackageName(),
+ /*databaseName=*/"database1",
+ /*callerAccess=*/mSelfCallerAccess)
+ .getSchemas())
.isEmpty();
results = mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- context.getPackageName(),
- /*visibilityStore=*/ null,
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mSelfCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).isEmpty();
// Make sure the index can now be used successfully
mAppSearchImpl.setSchema(
- context.getPackageName(),
+ mContext.getPackageName(),
"database1",
Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
// Insert a valid doc
mAppSearchImpl.putDocument(
- context.getPackageName(),
+ mContext.getPackageName(),
"database1",
validDoc,
/*logger=*/null);
@@ -520,10 +530,7 @@
results = mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- context.getPackageName(),
- /*visibilityStore=*/ null,
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mSelfCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).hasSize(1);
assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -551,9 +558,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -565,9 +570,7 @@
"package2",
"database2",
schema2,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -608,9 +611,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -622,9 +623,7 @@
"package2",
"database2",
schema2,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -660,16 +659,13 @@
}
@Test
- public void testGlobalQueryEmptyDatabase() throws Exception {
+ public void testGlobalQuery_emptyPackage() throws Exception {
SearchSpec searchSpec =
new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
- "",
+ /*queryExpression=*/"",
searchSpec,
- /*callerPackageName=*/ "",
- /*visibilityStore=*/ null,
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(/*callingPackageName=*/""),
/*logger=*/ null);
assertThat(searchResultPage.getResults()).isEmpty();
}
@@ -683,9 +679,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -727,9 +721,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -781,9 +773,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -801,9 +791,11 @@
.setTermMatch(TermMatchType.Code.PREFIX_VALUE)
.setResultCountPerPage(1)
.build();
- SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
- searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
- /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+ SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+ /*queryExpression=*/ "",
+ searchSpec,
+ new CallerAccess(/*callingPackageName=*/"package1"),
+ /*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
// most recent.
@@ -826,9 +818,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -846,9 +836,11 @@
.setTermMatch(TermMatchType.Code.PREFIX_VALUE)
.setResultCountPerPage(1)
.build();
- SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
- searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
- /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+ SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+ /*queryExpression=*/ "",
+ searchSpec,
+ new CallerAccess(/*callingPackageName=*/"package1"),
+ /*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
// most recent.
@@ -881,9 +873,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -932,9 +922,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -972,9 +960,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1026,9 +1012,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1046,9 +1030,11 @@
.setTermMatch(TermMatchType.Code.PREFIX_VALUE)
.setResultCountPerPage(1)
.build();
- SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
- searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
- /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+ SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+ /*queryExpression=*/ "",
+ searchSpec,
+ new CallerAccess(/*callingPackageName=*/"package1"),
+ /*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
// most recent.
@@ -1078,9 +1064,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1098,9 +1082,11 @@
.setTermMatch(TermMatchType.Code.PREFIX_VALUE)
.setResultCountPerPage(1)
.build();
- SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
- searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
- /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+ SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+ /*queryExpression=*/ "",
+ searchSpec,
+ new CallerAccess(/*callingPackageName=*/"package1"),
+ /*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
// most recent.
@@ -1155,9 +1141,7 @@
"package",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1193,9 +1177,7 @@
"package",
"database1",
oldSchemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1209,9 +1191,7 @@
"package",
"database1",
newSchemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1232,9 +1212,7 @@
"package",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1262,9 +1240,7 @@
"package",
"database1",
finalSchemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1276,9 +1252,7 @@
"package",
"database1",
finalSchemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1312,9 +1286,7 @@
"package",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1322,9 +1294,7 @@
"package",
"database2",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1356,9 +1326,7 @@
"package",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1397,9 +1365,7 @@
"package",
"database",
schema,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1444,6 +1410,13 @@
existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
}
+ // Create VisibilityDocument
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("schema")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+
// Insert schema for package A and B.
List<AppSearchSchema> schema =
ImmutableList.of(new AppSearchSchema.Builder("schema").build());
@@ -1451,9 +1424,7 @@
"packageA",
"database",
schema,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1461,14 +1432,12 @@
"packageB",
"database",
schema,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
- // Verify these two packages is stored in AppSearch
+ // Verify these two packages are stored in AppSearch.
SchemaProto expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
@@ -1483,6 +1452,26 @@
assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
.containsExactlyElementsIn(expectedTypes);
+ // Verify these two visibility documents are stored in AppSearch.
+ VisibilityDocument expectedVisibilityDocumentA =
+ new VisibilityDocument.Builder("packageA$database/schema")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ VisibilityDocument expectedVisibilityDocumentB =
+ new VisibilityDocument.Builder("packageB$database/schema")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility("packageA$database/schema"))
+ .isEqualTo(expectedVisibilityDocumentA);
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility("packageB$database/schema"))
+ .isEqualTo(expectedVisibilityDocumentB);
+
// Prune packages
mAppSearchImpl.prunePackageData(existingPackages);
@@ -1491,6 +1480,12 @@
.containsExactlyElementsIn(existingSchemas);
assertThat(mAppSearchImpl.getPackageToDatabases())
.containsExactlyEntriesIn(existingDatabases);
+
+ // Verify the VisibilitySetting is removed.
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility("packageA$database/schema")).isNull();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility("packageB$database/schema")).isNull();
}
@Test
@@ -1504,9 +1499,7 @@
mAppSearchImpl.setSchema(
"package1", "database1",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1518,9 +1511,7 @@
mAppSearchImpl.setSchema(
"package1", "database2",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1532,9 +1523,7 @@
mAppSearchImpl.setSchema(
"package2", "database1",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1542,6 +1531,47 @@
expectedMapping);
}
+ @Test
+ public void testGetAllPrefixedSchemaTypes() throws Exception {
+ // Insert schema
+ List<AppSearchSchema> schemas1 =
+ Collections.singletonList(new AppSearchSchema.Builder("type1").build());
+ List<AppSearchSchema> schemas2 =
+ Collections.singletonList(new AppSearchSchema.Builder("type2").build());
+ List<AppSearchSchema> schemas3 =
+ Collections.singletonList(new AppSearchSchema.Builder("type3").build());
+ mAppSearchImpl.setSchema(
+ "package1",
+ "database1",
+ schemas1,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ mAppSearchImpl.setSchema(
+ "package1",
+ "database2",
+ schemas2,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ mAppSearchImpl.setSchema(
+ "package2",
+ "database1",
+ schemas3,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ assertThat(mAppSearchImpl.getAllPrefixedSchemaTypes()).containsExactly(
+ "package1$database1/type1",
+ "package1$database2/type2",
+ "package2$database1/type3",
+ "VS#Pkg$VS#Db/VisibilityType", // plus the stored Visibility schema
+ "VS#Pkg$VS#Db/VisibilityPermissionType");
+ }
+
@FlakyTest(bugId = 204186664)
@Test
public void testReportUsage() throws Exception {
@@ -1552,9 +1582,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1642,9 +1670,7 @@
"package1",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1666,9 +1692,7 @@
"package1",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1683,9 +1707,7 @@
"package2",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1732,9 +1754,7 @@
"package1",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1756,9 +1776,7 @@
"package1",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1779,9 +1797,7 @@
"package1",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1789,9 +1805,7 @@
"package1",
"database2",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1834,9 +1848,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1848,15 +1860,15 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null));
assertThrows(IllegalStateException.class, () -> mAppSearchImpl.getSchema(
- "package", "database"));
+ /*packageName=*/"package",
+ /*databaseName=*/"database",
+ /*callerAccess=*/mSelfCallerAccess));
assertThrows(IllegalStateException.class, () -> mAppSearchImpl.putDocument(
"package",
@@ -1877,10 +1889,7 @@
assertThrows(IllegalStateException.class, () -> mAppSearchImpl.globalQuery(
"query",
new SearchSpec.Builder().build(),
- "package",
- /*visibilityStore=*/ null,
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mSelfCallerAccess,
/*logger=*/ null));
assertThrows(IllegalStateException.class, () -> mAppSearchImpl.getNextPage("package",
@@ -1922,9 +1931,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1945,7 +1952,8 @@
mAppSearchDir,
new UnlimitedLimitConfig(),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
"id1",
Collections.emptyMap());
@@ -1961,9 +1969,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2004,7 +2010,8 @@
mAppSearchDir,
new UnlimitedLimitConfig(),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
"database",
"namespace1",
@@ -2025,9 +2032,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2070,7 +2075,8 @@
mAppSearchDir,
new UnlimitedLimitConfig(),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
"database",
"namespace1",
@@ -2091,9 +2097,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2116,7 +2120,7 @@
.isEqualTo(2);
assertThat(
storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
- .isEqualTo(1);
+ .isEqualTo(3); // +2 for VisibilitySchema
}
@Test
@@ -2136,7 +2140,8 @@
return 1;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas =
@@ -2145,9 +2150,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2196,7 +2199,8 @@
return 1;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas =
@@ -2205,9 +2209,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2243,7 +2245,8 @@
return 1;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Make sure the limit is maintained
e = assertThrows(AppSearchException.class, () ->
@@ -2270,7 +2273,8 @@
return 3;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas =
@@ -2279,9 +2283,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2360,7 +2362,8 @@
return 2;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas =
@@ -2369,9 +2372,7 @@
"package1",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2379,9 +2380,7 @@
"package1",
"database2",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2389,9 +2388,7 @@
"package2",
"database1",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2399,9 +2396,7 @@
"package2",
"database2",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2451,7 +2446,8 @@
return 2;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// package1 should still be out of space
e = assertThrows(AppSearchException.class, () ->
@@ -2500,7 +2496,8 @@
return 3;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2516,9 +2513,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2614,7 +2609,8 @@
return 2;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2626,9 +2622,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2685,7 +2679,8 @@
return 2;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2697,9 +2692,7 @@
"package",
"database",
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2736,7 +2729,8 @@
return 2;
}
},
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
// Index id2. This should pass but only because we check for replacements.
mAppSearchImpl.putDocument(
@@ -2754,4 +2748,1511 @@
assertThat(e).hasMessageThat().contains(
"Package \"package\" exceeded limit of 2 documents");
}
+
+ /**
+ * Ensure that it is okay to register the same observer for multiple packages and that removing
+ * the observer for one package doesn't remove it for the other.
+ */
+ @Test
+ public void testRemoveObserver_onlyAffectsOnePackage() throws Exception {
+ final String fakePackage = "com.android.appsearch.fake.package";
+
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ /*schemas=*/ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/false,
+ /*version=*/0,
+ /*setSchemaStatsBuilder=*/null);
+
+ // Register an observer twice, on different packages.
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/fakePackage,
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a valid doc
+ GenericDocument validDoc =
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ validDoc,
+ /*logger=*/null);
+
+ // Dispatch notifications and empty the observers
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ observer.clear();
+
+ // Remove the observer from the fake package
+ mAppSearchImpl.unregisterObserverCallback(fakePackage, observer);
+
+ // Index a second document
+ GenericDocument doc2 = new GenericDocument.Builder<>("namespace1", "id2", "Type1").build();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ doc2,
+ /*logger=*/null);
+
+ // Observer should still have received this data from its registration on
+ // context.getPackageName(), as we only removed the copy from fakePackage.
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).containsExactly(
+ new DocumentChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ "namespace1",
+ "Type1",
+ /*changedDocumentIds=*/ImmutableSet.of("id2")));
+ }
+
+ @Test
+ public void testGetGlobalDocumentThrowsExceptionWhenNotVisible() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+
+ // Add a document and persist it.
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+ mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+ mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+ AppSearchException e = assertThrows(AppSearchException.class, () ->
+ mAppSearchImpl.globalGetDocument(
+ "package",
+ "database",
+ "namespace1",
+ "id1",
+ /*typePropertyPaths=*/Collections.emptyMap(),
+ /*callerAccess=*/mSelfCallerAccess));
+ assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+ assertThat(e.getMessage()).isEqualTo("Document (namespace1, id1) not found.");
+ }
+
+ @Test
+ public void testGetGlobalDocument() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+
+ // Add a document and persist it.
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+ mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+ mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+ GenericDocument getResult = mAppSearchImpl.globalGetDocument(
+ "package",
+ "database",
+ "namespace1",
+ "id1",
+ /*typePropertyPaths=*/Collections.emptyMap(),
+ /*callerAccess=*/mSelfCallerAccess);
+ assertThat(getResult).isEqualTo(document);
+ }
+
+ @Test
+ public void getGlobalDocumentTest_notFound() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+
+ // Add a document and persist it.
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+ mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+ mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+ AppSearchException e = assertThrows(AppSearchException.class, () ->
+ mAppSearchImpl.globalGetDocument(
+ "package",
+ "database",
+ "namespace1",
+ "id2",
+ /*typePropertyPaths=*/Collections.emptyMap(),
+ /*callerAccess=*/mSelfCallerAccess));
+ assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+ assertThat(e.getMessage()).isEqualTo("Document (namespace1, id2) not found.");
+ }
+
+ @Test
+ public void getGlobalDocumentNoAccessNoFileHasSameException() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) ->
+ callerAccess.getCallingPackageName().equals("visiblePackage");
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+
+ // Add a document and persist it.
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+ mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+ mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+ AppSearchException unauthorizedException = assertThrows(AppSearchException.class, () ->
+ mAppSearchImpl.globalGetDocument(
+ "package",
+ "database",
+ "namespace1",
+ "id1",
+ /*typePropertyPaths=*/Collections.emptyMap(),
+ new CallerAccess(/*callingPackageName=*/"invisiblePackage")));
+
+ mAppSearchImpl.remove("package", "database", "namespace1", "id1",
+ /*removeStatsBuilder=*/null);
+
+ AppSearchException noDocException = assertThrows(AppSearchException.class, () ->
+ mAppSearchImpl.globalGetDocument(
+ "package",
+ "database",
+ "namespace1",
+ "id1",
+ /*typePropertyPaths=*/Collections.emptyMap(),
+ new CallerAccess(/*callingPackageName=*/"visiblePackage")));
+
+ assertThat(noDocException.getResultCode()).isEqualTo(unauthorizedException.getResultCode());
+ assertThat(noDocException.getMessage()).isEqualTo(unauthorizedException.getMessage());
+ }
+
+ @Test
+ public void testSetVisibility() throws Exception {
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+
+ // Set schema Email to AppSearch database1 with a visibility document
+ mAppSearchImpl.setSchema(
+ "package",
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ String prefix = PrefixUtil.createPrefix("package", "database1");
+
+ // assert the visibility document is saved.
+ VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ .isEqualTo(expectedDocument);
+ }
+
+ @Test
+ public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
+ // Create Visibility Document for Email1
+ VisibilityDocument visibilityDocument1 = new VisibilityDocument.Builder("Email1")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ List<AppSearchSchema> schemas1 =
+ Collections.singletonList(new AppSearchSchema.Builder("Email1").build());
+
+ // Set schema Email1 to package1 with a visibility document
+ mAppSearchImpl.setSchema(
+ "package1",
+ "database",
+ schemas1,
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument1),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+
+ // assert the visibility document is saved.
+ VisibilityDocument expectedDocument1 = new VisibilityDocument.Builder(prefix1 + "Email1")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+ .isEqualTo(expectedDocument1);
+
+ // Create Visibility Document for Email2
+ VisibilityDocument visibilityDocument2 = new VisibilityDocument.Builder("Email2")
+ .setNotDisplayedBySystem(false)
+ .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+ .setCreationTimestampMillis(54321L)
+ .build();
+ List<AppSearchSchema> schemas2 =
+ Collections.singletonList(new AppSearchSchema.Builder("Email2").build());
+
+ // Set schema Email2 to package1 with a visibility document
+ mAppSearchImpl.setSchema(
+ "package2",
+ "database",
+ schemas2,
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument2),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+ // assert the visibility document is saved.
+ VisibilityDocument expectedDocument2 = new VisibilityDocument.Builder(prefix2 + "Email2")
+ .setNotDisplayedBySystem(false)
+ .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+ .setCreationTimestampMillis(54321)
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
+ .isEqualTo(expectedDocument2);
+
+ // Check the existing visibility document retains.
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+ .isEqualTo(expectedDocument1);
+ }
+
+ @Test
+ public void testSetVisibility_removeVisibilitySettings() throws Exception {
+ // Create a non-all-default visibility document
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+
+ // Set schema Email and its visibility document to AppSearch database1
+ mAppSearchImpl.setSchema(
+ "package",
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ String prefix = PrefixUtil.createPrefix("package", "database1");
+ VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ .isEqualTo(expectedDocument);
+
+ // Set schema Email and its all-default visibility document to AppSearch database1
+ mAppSearchImpl.setSchema(
+ "package",
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+ // All-default visibility document won't be saved in AppSearch.
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ .isNull();
+ }
+
+ @Test
+ public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
+ // set Schema and visibility to AppSearch
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+ mAppSearchImpl.setSchema(
+ "packageName",
+ "databaseName",
+ schemas,
+ ImmutableList.of(visibilityDocument),
+ /*forceOverride=*/ true,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // close and re-open AppSearchImpl, the visibility document retains
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+
+ String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+ VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .setCreationTimestampMillis(12345L)
+ .build();
+
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ .isEqualTo(expectedDocument);
+
+ // remove schema and visibility document
+ mAppSearchImpl.setSchema(
+ "packageName",
+ "databaseName",
+ ImmutableList.of(),
+ ImmutableList.of(),
+ /*forceOverride=*/ true,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // close and re-open AppSearchImpl, the visibility document removed
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
+ }
+
+ @Test
+ public void testGetSchema_global() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Type").build());
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ImmutableList.of(
+ new VisibilityDocument.Builder("Type")
+ .setNotDisplayedBySystem(true).build()),
+ /*forceOverride=*/false,
+ /*version=*/0,
+ /*setSchemaStatsBuilder=*/null);
+
+ // Get this schema as another package
+ GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(/*callingPackageName=*/"com.android.appsearch.fake.package"));
+ assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+ assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("Type");
+ }
+
+ @Test
+ public void testGetSchema_nonExistentApp() throws Exception {
+ // Add a schema. The test loses meaning if the schema is completely empty.
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
+ /*visibilityDocuments=*/ImmutableList.of(),
+ /*forceOverride=*/false,
+ /*version=*/0,
+ /*setSchemaStatsBuilder=*/null);
+
+ // Try to get the schema of a nonexistent package.
+ GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+ "com.android.appsearch.fake.package",
+ "database",
+ new CallerAccess(/*callingPackageName=*/"package"));
+ assertThat(getResponse.getSchemas()).isEmpty();
+ assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+ }
+
+ @Test
+ public void testGetSchema_noAccess() throws Exception {
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Type").build());
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ImmutableList.of(),
+ /*forceOverride=*/false,
+ /*version=*/1,
+ /*setSchemaStatsBuilder=*/null);
+ GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(/*callingPackageName=*/
+ "com.android.appsearch.fake.package"));
+ assertThat(getResponse.getSchemas()).isEmpty();
+ assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+ assertThat(getResponse.getVersion()).isEqualTo(0);
+
+ // Make sure the test is hooked up right by calling getSchema with the same parameters but
+ // from the same package
+ getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(/*callingPackageName=*/"package"));
+ assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+ }
+
+ @Test
+ public void testGetSchema_global_partialAccess() throws Exception {
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("VisibleType").build(),
+ new AppSearchSchema.Builder("PrivateType").build());
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+ VisibilityChecker mockVisibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> prefixedSchema.endsWith("VisibleType");
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ mockVisibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ /*visibilityDocuments=*/ImmutableList.of(
+ new VisibilityDocument.Builder("VisibleType")
+ .setNotDisplayedBySystem(true)
+ .build(),
+ new VisibilityDocument.Builder("PrivateType")
+ .setNotDisplayedBySystem(true)
+ .build()),
+ /*forceOverride=*/false,
+ /*version=*/1,
+ /*setSchemaStatsBuilder=*/null);
+
+ GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(/*callingPackageName=*/
+ "com.android.appsearch.fake.package"));
+ assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
+ assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("VisibleType");
+ assertThat(getResponse.getVersion()).isEqualTo(1);
+ }
+
+ @Test
+ public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a valid doc
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+ /*logger=*/null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).containsExactly(
+ new DocumentChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ "namespace1",
+ "Type1",
+ ImmutableSet.of("id1")));
+ }
+
+ @Test
+ public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
+ // Make a visibility checker that rejects everything
+ final VisibilityChecker rejectChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ rejectChecker);
+
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a valid doc
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+ /*logger=*/null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).containsExactly(
+ new DocumentChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ "namespace1",
+ "Type1",
+ ImmutableSet.of("id1")));
+ }
+
+ @Test
+ public void testDispatchObserver_differentPackage_noVisStore_reject() throws Exception {
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer from a simulated different package
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/
+ "com.fake.Listening.package"),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a valid doc
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+ /*logger=*/null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testDispatchObserver_differentPackage_withVisStore_accept() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+
+ // Make a visibility checker that allows only fakeListeningPackage.
+ final VisibilityChecker visibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a valid doc
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+ /*logger=*/null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).containsExactly(
+ new DocumentChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ "namespace1",
+ "Type1",
+ ImmutableSet.of("id1")));
+ }
+
+ @Test
+ public void testDispatchObserver_differentPackage_withVisStore_reject() throws Exception {
+ final String fakeListeningPackage = "com.fake.Listening.package";
+
+ // Make a visibility checker that rejects everything.
+ final VisibilityChecker rejectChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ rejectChecker);
+
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Insert a doc
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.putDocument(
+ mContext.getPackageName(),
+ "database1",
+ new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+ /*logger=*/null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_added() throws Exception {
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Add a schema type
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type1")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Add two more schema types without touching the existing one
+ observer.clear();
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build(),
+ new AppSearchSchema.Builder("Type3").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2", "Type3")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_removed() throws Exception {
+ // Add a schema type
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Remove Type2
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents() throws Exception {
+ // Add a schema
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update the schema, but don't make any actual changes
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 1,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Now update the schema again, but this time actually make a change (cardinality of the
+ // property)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 2,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+ // Add a schema
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer that only listens for Type2
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update both types of the schema (changed cardinalities)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_visibilityOnly() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+
+ // Make a fake visibility checker that actually looks at visibility store
+ final VisibilityChecker visibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> {
+ if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
+ return false;
+ }
+ Set<String> allowedPackages = new ArraySet<>(
+ visibilityStore.getVisibility(prefixedSchema).getPackageNames());
+ return allowedPackages.contains(fakeListeningPackage);
+ };
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Add a schema where both types are visible to the fake package.
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build());
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build()
+ ),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Notifications of addition should now be dispatched
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type1", "Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ observer.clear();
+
+ // Update schema, keeping the types identical but denying visibility to type2
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2").build()
+ ),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. This should look like a deletion of Type2.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ observer.clear();
+
+ // Now update Type2 and make sure no further notification is received.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2").build()
+ ),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Grant visibility to Type2 again and make sure it appears
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build()
+ ),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. This should look like a creation of Type2.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_visibilityAndContents() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+
+ // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+ final VisibilityChecker visibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+ && prefixedSchema.endsWith("Type2");
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update both types of the schema (changed cardinalities)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_partialVisibility_removed() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+
+ // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+ final VisibilityChecker visibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+ && prefixedSchema.endsWith("Type2");
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Remove Type1
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. Nothing should appear since Type1 is not visible to us.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Now remove Type2. This should cause a notification.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_multipleObservers() throws Exception {
+ // Create two fake packages. One can access Type1, one can access Type2, they both can
+ // access Type3, and no one can access Type4.
+ final String fakePackage1 = "com.fake.listening.package1";
+
+ final String fakePackage2 = "com.fake.listening.package2";
+
+ final VisibilityChecker visibilityChecker =
+ (callerAccess, packageName, prefixedSchema, visibilityStore)
+ -> {
+ if (prefixedSchema.endsWith("Type1")) {
+ return callerAccess.getCallingPackageName().equals(fakePackage1);
+ } else if (prefixedSchema.endsWith("Type2")) {
+ return callerAccess.getCallingPackageName().equals(fakePackage2);
+ } else if (prefixedSchema.endsWith("Type3")) {
+ return false;
+ } else if (prefixedSchema.endsWith("Type4")) {
+ return true;
+ } else {
+ throw new IllegalArgumentException(prefixedSchema);
+ }
+ };
+ mAppSearchImpl.close();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build(),
+ new AppSearchSchema.Builder("Type3").build(),
+ new AppSearchSchema.Builder("Type4").build()
+ ),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register three observers: one in each package, and another in package1 with a filter.
+ TestObserverCallback observerPkg1NoFilter = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakePackage1),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observerPkg1NoFilter);
+
+ TestObserverCallback observerPkg2NoFilter = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakePackage2),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observerPkg2NoFilter);
+
+ TestObserverCallback observerPkg1FilterType4 = new TestObserverCallback();
+ mAppSearchImpl.registerObserverCallback(
+ new CallerAccess(/*callingPackageName=*/fakePackage1),
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("Type4").build(),
+ MoreExecutors.directExecutor(),
+ observerPkg1FilterType4);
+
+ // Remove everything
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications.
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+
+ // observerPkg1NoFilter should see Type1 and Type4 vanish.
+ // observerPkg2NoFilter should see Type2 and Type4 vanish.
+ // observerPkg2WithFilter should see Type4 vanish.
+ assertThat(observerPkg1NoFilter.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type1", "Type4"))
+ );
+ assertThat(observerPkg1NoFilter.getDocumentChanges()).isEmpty();
+
+ assertThat(observerPkg2NoFilter.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2", "Type4"))
+ );
+ assertThat(observerPkg2NoFilter.getDocumentChanges()).isEmpty();
+
+ assertThat(observerPkg1FilterType4.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type4"))
+ );
+ assertThat(observerPkg1FilterType4.getDocumentChanges()).isEmpty();
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
index 75966bf..bf2ddbd 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -74,7 +74,8 @@
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
mLogger = new SimpleTestLogger();
}
@@ -335,16 +336,18 @@
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
initStatsBuilder,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
InitializeStats iStats = initStatsBuilder.build();
appSearchImpl.close();
assertThat(iStats).isNotNull();
+ // If the process goes really fast, the total latency could be 0. Since the default of total
+ // latency is also 0, we just remove the assert about NativeLatencyMillis.
assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
// Total latency captured in LocalStorage
assertThat(iStats.getTotalLatencyMillis()).isEqualTo(0);
assertThat(iStats.hasDeSync()).isFalse();
- assertThat(iStats.getNativeLatencyMillis()).isGreaterThan(0);
assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
assertThat(iStats.getDocumentCount()).isEqualTo(0);
@@ -363,7 +366,8 @@
folder,
new UnlimitedLimitConfig(),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE);
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build());
@@ -371,9 +375,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -388,19 +390,21 @@
// Create another appsearchImpl on the same folder
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+ folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
InitializeStats iStats = initStatsBuilder.build();
assertThat(iStats).isNotNull();
+ // If the process goes really fast, the total latency could be 0. Since the default of total
+ // latency is also 0, we just remove the assert about NativeLatencyMillis.
assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
// Total latency captured in LocalStorage
assertThat(iStats.getTotalLatencyMillis()).isEqualTo(0);
assertThat(iStats.hasDeSync()).isFalse();
- assertThat(iStats.getNativeLatencyMillis()).isGreaterThan(0);
assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
assertThat(iStats.getDocumentCount()).isEqualTo(2);
- assertThat(iStats.getSchemaTypeCount()).isEqualTo(2);
+ assertThat(iStats.getSchemaTypeCount()).isEqualTo(4); // +2 for VisibilitySchema
assertThat(iStats.hasReset()).isEqualTo(false);
assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
appSearchImpl.close();
@@ -413,7 +417,8 @@
final File folder = mTemporaryFolder.newFolder();
AppSearchImpl appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
@@ -422,9 +427,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -447,7 +450,8 @@
// Create another appsearchImpl on the same folder
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+ folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
InitializeStats iStats = initStatsBuilder.build();
// Some of other fields are already covered by AppSearchImplTest#testReset()
@@ -475,9 +479,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -517,9 +519,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -558,9 +558,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -598,10 +596,11 @@
SearchStats sStats = mLogger.mSearchStats;
assertThat(sStats).isNotNull();
+ // If the process goes really fast, the total latency could be 0. Since the default of total
+ // latency is also 0, we just remove the assert about TotalLatencyMillis.
assertThat(sStats.getPackageName()).isEqualTo(testPackageName);
assertThat(sStats.getDatabase()).isEqualTo(testDatabase);
assertThat(sStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
- assertThat(sStats.getTotalLatencyMillis()).isGreaterThan(0);
assertThat(sStats.getVisibilityScope()).isEqualTo(SearchStats.VISIBILITY_SCOPE_LOCAL);
assertThat(sStats.getTermCount()).isEqualTo(2);
assertThat(sStats.getQueryLength()).isEqualTo(queryStr.length());
@@ -626,9 +625,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -664,9 +661,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -699,9 +694,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -739,9 +732,7 @@
testPackageName,
testDatabase,
schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -784,9 +775,7 @@
PACKAGE_NAME,
DATABASE,
Collections.singletonList(schema1),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -798,9 +787,7 @@
PACKAGE_NAME,
DATABASE,
Collections.singletonList(schema2),
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ sStatsBuilder);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
index 9df244e..4441f2a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
@@ -51,7 +51,8 @@
mAppSearchImpl = AppSearchImpl.create(
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
}
@After
@@ -68,9 +69,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -95,12 +94,12 @@
searchSpec,
/*logger=*/ null);
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(1);
assertThat(results.get(0).getGenericDocument()).isEqualTo(document1);
// We get all documents, and it shouldn't fail if we keep calling getNextPage().
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
assertThat(results).isEmpty();
}
@@ -113,9 +112,7 @@
"package1",
"database1",
schema1,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -146,18 +143,18 @@
searchSpec,
/*logger=*/ null);
List<GenericDocument> outDocs = new ArrayList<>();
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(2);
outDocs.add(results.get(0).getGenericDocument());
outDocs.add(results.get(1).getGenericDocument());
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(1);
outDocs.add(results.get(0).getGenericDocument());
assertThat(outDocs).containsExactly(document1, document2, document3);
// We get all documents, and it shouldn't fail if we keep calling getNextPage().
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
assertThat(results).isEmpty();
}
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index 29aeb96..85f46c6 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -23,12 +23,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.annotation.NonNull;
-import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.testutil.AppSearchTestUtils;
import com.google.android.icing.proto.ResultSpecProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -37,13 +39,19 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
-import java.util.List;
import java.util.Map;
import java.util.Set;
public class SearchSpecToProtoConverterTest {
+ /** An optimize strategy that always triggers optimize. */
+ public static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+ @Rule
+ public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
@Test
public void testToSearchSpecProto() throws Exception {
@@ -117,7 +125,7 @@
assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
assertThat(resultSpecProto.getSnippetSpec().getNumMatchesPerProperty()).isEqualTo(345);
- assertThat(resultSpecProto.getSnippetSpec().getMaxWindowBytes()).isEqualTo(456);
+ assertThat(resultSpecProto.getSnippetSpec().getMaxWindowUtf32Length()).isEqualTo(456);
}
@Test
@@ -423,14 +431,20 @@
}
@Test
- public void testRemoveInaccessibleSchemaFilter() {
- String prefix = PrefixUtil.createPrefix("package", "database");
+ public void testRemoveInaccessibleSchemaFilter() throws Exception {
+ AppSearchImpl appSearchImpl = AppSearchImpl.create(
+ mTemporaryFolder.newFolder(),
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+ VisibilityStore visibilityStore = new VisibilityStore(appSearchImpl);
- SearchSpec searchSpec = new SearchSpec.Builder().build();
-
+ final String prefix = PrefixUtil.createPrefix("package", "database");
SchemaTypeConfigProto schemaTypeConfigProto =
SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
- SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ new SearchSpec.Builder().build(),
/*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(
prefix, ImmutableSet.of("package$database/namespace1")),
@@ -441,27 +455,11 @@
"package$database/schema3", schemaTypeConfigProto)));
converter.removeInaccessibleSchemaFilter(
- /*callerPackageName=*/"otherPackageName",
- new VisibilityStore() {
- @Override
- public void setVisibility(@NonNull String packageName,
- @NonNull String databaseName,
- @NonNull Set<String> schemasNotDisplayedBySystem,
- @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
- throws AppSearchException {
-
- }
-
- @Override
- public boolean isSchemaSearchableByCaller(@NonNull String packageName,
- @NonNull String databaseName, @NonNull String prefixedSchema,
- int callerUid, boolean callerHasSystemAccess) {
- // filter out schema 2 which is not searchable for user.
- return !prefixedSchema.equals(prefix + "schema2");
- }
- },
- /*callerUid=*/-1,
- /*callerHasSystemAccess=*/true);
+ new CallerAccess(/*callingPackageName=*/"otherPackageName"),
+ visibilityStore,
+ AppSearchTestUtils.createMockVisibilityChecker(
+ /*visiblePrefixedSchemas=*/ ImmutableSet.of(
+ prefix + "schema1", prefix + "schema3")));
SearchSpecProto searchSpecProto =
converter.toSearchSpecProto(/*queryExpression=*/"");
@@ -506,27 +504,9 @@
// remove all target schema filter, and the query becomes nothing to search.
nonEmptyConverter.removeInaccessibleSchemaFilter(
- /*callerPackageName=*/"otherPackageName",
- new VisibilityStore() {
- @Override
- public void setVisibility(@NonNull String packageName,
- @NonNull String databaseName,
- @NonNull Set<String> schemasNotDisplayedBySystem,
- @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
- throws AppSearchException {
-
- }
-
- @Override
- public boolean isSchemaSearchableByCaller(@NonNull String packageName,
- @NonNull String databaseName, @NonNull String prefixedSchema,
- int callerUid, boolean callerHasSystemAccess) {
- // filter out all schema.
- return false;
- }
- },
- /*callerUid=*/-1,
- /*callerHasSystemAccess=*/true);
+ new CallerAccess(/*callingPackageName=*/"otherPackageName"),
+ /*visibilityStore=*/null,
+ /*visibilityChecker=*/null);
assertThat(nonEmptyConverter.isNothingToSearch()).isTrue();
}
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
new file mode 100644
index 0000000..ec88a88
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_NAME_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_SCHEMA_TYPE;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_SHA_256_CERT_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBILITY_SCHEMA_TYPE;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+
+public class VisibilityStoreMigrationHelperFromV0Test {
+
+ /**
+ * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+ */
+ private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private File mFile;
+
+ @Before
+ public void setUp() throws Exception {
+ // Give ourselves global query permissions
+ mFile = mTemporaryFolder.newFolder();
+ }
+
+ @Test
+ public void testVisibilityMigration_from0() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[32];
+
+ // Values for a "bar" client
+ String packageNameBar = "packageBar";
+ byte[] sha256CertBar = new byte[32];
+
+ // Create AppSearchImpl with visibility document version 0;
+ AppSearchImpl appSearchImplInV0 = buildAppSearchImplInV0();
+ // Build deprecated visibility documents in version 0
+ // "schema1" and "schema2" are platform hidden.
+ // "schema1" is accessible to packageFoo and "schema2" is accessible to packageBar.
+ String prefix = PrefixUtil.createPrefix("package", "database");
+ GenericDocument deprecatedVisibilityToPackageFoo = new GenericDocument.Builder<>(
+ VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+ .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema1")
+ .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameFoo)
+ .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertFoo)
+ .build();
+ GenericDocument deprecatedVisibilityToPackageBar = new GenericDocument.Builder<>(
+ VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+ .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema2")
+ .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameBar)
+ .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertBar)
+ .build();
+ GenericDocument deprecatedVisibilityDocument = new GenericDocument.Builder<>(
+ VisibilityDocument.NAMESPACE,
+ VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
+ "package", "database"),
+ DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+ .setPropertyString(DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+ prefix + "Schema1", prefix + "Schema2")
+ .setPropertyDocument(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+ deprecatedVisibilityToPackageFoo, deprecatedVisibilityToPackageBar)
+ .build();
+
+ // Set some client schemas into AppSearchImpl with empty VisibilityDocument since we need to
+ // directly put old version of VisibilityDocument.
+ appSearchImplInV0.setSchema(
+ "package",
+ "database",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("schema1").build(),
+ new AppSearchSchema.Builder("schema2").build()),
+ /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*schemaVersion=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Put deprecated visibility documents in version 0 to AppSearchImpl
+ appSearchImplInV0.putDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ deprecatedVisibilityDocument,
+ /*logger=*/null);
+
+ // Persist to disk and re-open the AppSearchImpl
+ appSearchImplInV0.close();
+ AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+
+ VisibilityDocument actualDocument1 = new VisibilityDocument(
+ appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ /*id=*/ prefix + "Schema1",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ VisibilityDocument actualDocument2 = new VisibilityDocument(
+ appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ /*id=*/ prefix + "Schema2",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+
+ VisibilityDocument expectedDocument1 =
+ new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+ .setNotDisplayedBySystem(true)
+ .setCreationTimestampMillis(actualDocument1.getCreationTimestampMillis())
+ .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
+ .build();
+ VisibilityDocument expectedDocument2 =
+ new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+ .setNotDisplayedBySystem(true)
+ .setCreationTimestampMillis(actualDocument2.getCreationTimestampMillis())
+ .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
+ .build();
+ assertThat(actualDocument1).isEqualTo(expectedDocument1);
+ assertThat(actualDocument2).isEqualTo(expectedDocument2);
+ }
+
+ /** Build AppSearchImpl with deprecated visibility schemas version 0. */
+ private AppSearchImpl buildAppSearchImplInV0() throws Exception {
+ AppSearchSchema visibilityDocumentSchemaV0 = new AppSearchSchema.Builder(
+ DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+ DEPRECATED_PACKAGE_SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+ AppSearchSchema visibilityToPackagesSchemaV0 = new AppSearchSchema.Builder(
+ DEPRECATED_PACKAGE_SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ DEPRECATED_PACKAGE_NAME_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ DEPRECATED_SHA_256_CERT_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+ // Set deprecated visibility schema version 0 into AppSearchImpl.
+ AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+ appSearchImpl.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ ImmutableList.of(visibilityDocumentSchemaV0, visibilityToPackagesSchemaV0),
+ /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+ /*forceOverride=*/ true, // force push the old version into disk
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ return appSearchImpl;
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
new file mode 100644
index 0000000..c85a7a9
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV1.DEPRECATED_ROLE_ASSISTANT;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV1.DEPRECATED_ROLE_HOME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+
+public class VisibilityStoreMigrationHelperFromV1Test {
+
+ /**
+ * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+ */
+ private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private File mFile;
+
+ @Before
+ public void setUp() throws Exception {
+ // Give ourselves global query permissions
+ mFile = mTemporaryFolder.newFolder();
+ }
+
+ @Test
+ public void testVisibilityMigration_from1() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[32];
+
+ // Values for a "bar" client
+ String packageNameBar = "packageBar";
+ byte[] sha256CertBar = new byte[32];
+
+ // Create AppSearchImpl with visibility document version 1;
+ AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+ appSearchImplInV1.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ ImmutableList.of(VisibilityDocumentV1.SCHEMA),
+ /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+ /*forceOverride=*/ true, // force push the old version into disk
+ /*version=*/ 1,
+ /*setSchemaStatsBuilder=*/ null);
+ // Build deprecated visibility documents in version 1
+ String prefix = PrefixUtil.createPrefix("package", "database");
+ VisibilityDocumentV1 visibilityDocumentV1 =
+ new VisibilityDocumentV1.Builder(prefix + "Schema")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
+ .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
+ .setVisibleToRoles(ImmutableSet.of(DEPRECATED_ROLE_HOME,
+ DEPRECATED_ROLE_ASSISTANT))
+ .setVisibleToPermissions(ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to
+ // directly put old version of VisibilityDocument.
+ appSearchImplInV1.setSchema(
+ "package",
+ "database",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Schema").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*schemaVersion=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Put deprecated visibility documents in version 0 to AppSearchImpl
+ appSearchImplInV1.putDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ visibilityDocumentV1,
+ /*logger=*/null);
+
+ // Persist to disk and re-open the AppSearchImpl
+ appSearchImplInV1.close();
+ AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+
+ VisibilityDocument actualDocument = new VisibilityDocument(
+ appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ /*id=*/ prefix + "Schema",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+
+ assertThat(actualDocument.isNotDisplayedBySystem()).isTrue();
+ assertThat(actualDocument.getPackageNames()).asList().containsExactly(packageNameFoo,
+ packageNameBar);
+ assertThat(actualDocument.getSha256Certs()).isEqualTo(
+ new byte[][] {sha256CertFoo, sha256CertBar});
+ assertThat(actualDocument.getVisibleToPermissions()).containsExactlyElementsIn(
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),
+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
new file mode 100644
index 0000000..d31247c
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+
+public class VisibilityStoreTest {
+
+ /**
+ * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+ */
+ private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private File mAppSearchDir;
+ private AppSearchImpl mAppSearchImpl;
+ private VisibilityStore mVisibilityStore;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppSearchDir = mTemporaryFolder.newFolder();
+ mAppSearchImpl = AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+ mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+ }
+
+ @After
+ public void tearDown() {
+ mAppSearchImpl.close();
+ }
+
+ /**
+ * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+ */
+ @Test
+ public void testValidPackageName() {
+ assertThat(VisibilityStore.VISIBILITY_PACKAGE_NAME)
+ .doesNotContain(String.valueOf(PrefixUtil.PACKAGE_DELIMITER));
+ assertThat(VisibilityStore.VISIBILITY_PACKAGE_NAME)
+ .doesNotContain(String.valueOf(PrefixUtil.DATABASE_DELIMITER));
+ }
+
+ /**
+ * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+ */
+ @Test
+ public void testValidDatabaseName() {
+ assertThat(VisibilityStore.VISIBILITY_DATABASE_NAME)
+ .doesNotContain(String.valueOf(PrefixUtil.PACKAGE_DELIMITER));
+ assertThat(VisibilityStore.VISIBILITY_DATABASE_NAME)
+ .doesNotContain(String.valueOf(PrefixUtil.DATABASE_DELIMITER));
+ }
+
+ @Test
+ public void testSetAndGetVisibility() throws Exception {
+ String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+ assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+ .isEqualTo(visibilityDocument);
+ }
+
+ @Test
+ public void testRemoveVisibility() throws Exception {
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+ assertThat(mVisibilityStore.getVisibility("Email"))
+ .isEqualTo(visibilityDocument);
+
+ mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityDocument.getId()));
+ assertThat(mVisibilityStore.getVisibility("Email")).isNull();
+ }
+
+ @Test
+ public void testRecoverBrokenVisibilitySchema() throws Exception {
+ // Create a broken schema which could be recovered to the latest schema in a compatible
+ // change. Since we won't set force override to true to recover the broken case.
+ AppSearchSchema brokenSchema = new AppSearchSchema.Builder(VisibilityDocument.SCHEMA_TYPE)
+ .build();
+
+ // Index a broken schema into AppSearch, use the latest version to make it broken.
+ mAppSearchImpl.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ Collections.singletonList(brokenSchema),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ // Create VisibilityStore should recover the broken schema
+ mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+ // We should be able to set and get Visibility settings.
+ String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+ VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+ assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+ .isEqualTo(visibilityDocument);
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index 23b0e83..c7bc830 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java)
package androidx.appsearch.localstorage;
import androidx.annotation.NonNull;
@@ -33,7 +34,16 @@
if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
return true;
}
- if (Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER.equals(feature)) {
+ if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
+ return true;
+ }
+ if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
+ return true;
+ }
+ if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
+ return true;
+ }
+ if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
return true;
}
return false;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index a0016a2..acaa566 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -16,6 +16,8 @@
package androidx.appsearch.localstorage;
+import static androidx.appsearch.app.AppSearchResult.RESULT_INTERNAL_ERROR;
+import static androidx.appsearch.app.AppSearchResult.RESULT_SECURITY_ERROR;
import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
@@ -43,6 +45,7 @@
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter;
@@ -58,8 +61,11 @@
import androidx.appsearch.localstorage.stats.SearchStats;
import androidx.appsearch.localstorage.stats.SetSchemaStats;
import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
import androidx.appsearch.util.LogUtil;
import androidx.collection.ArrayMap;
@@ -211,6 +217,21 @@
private final ObserverManager mObserverManager = new ObserverManager();
/**
+ * VisibilityStore will be used in {@link #setSchema} and {@link #getSchema} to store and query
+ * visibility information. But to create a {@link VisibilityStore}, it will call
+ * {@link #setSchema} and {@link #getSchema} to get the visibility schema. Make it nullable to
+ * avoid call it before we actually create it.
+ */
+ @Nullable
+ @VisibleForTesting
+ @GuardedBy("mReadWriteLock")
+ final VisibilityStore mVisibilityStoreLocked;
+
+ @Nullable
+ @GuardedBy("mReadWriteLock")
+ private final VisibilityChecker mVisibilityCheckerLocked;
+
+ /**
* The counter to check when to call {@link #checkForOptimize}. The
* interval is
* {@link #CHECK_OPTIMIZE_INTERVAL}.
@@ -234,15 +255,20 @@
* and putDocument.
*
* @param initStatsBuilder collects stats for initialization if provided.
+ * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has
+ * access to aa specific schema. Pass null will lost that ability and
+ * global querier could only get their own data.
*/
@NonNull
public static AppSearchImpl create(
@NonNull File icingDir,
@NonNull LimitConfig limitConfig,
@Nullable InitializeStats.Builder initStatsBuilder,
- @NonNull OptimizeStrategy optimizeStrategy)
+ @NonNull OptimizeStrategy optimizeStrategy,
+ @Nullable VisibilityChecker visibilityChecker)
throws AppSearchException {
- return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy);
+ return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy,
+ visibilityChecker);
}
/**
@@ -252,11 +278,13 @@
@NonNull File icingDir,
@NonNull LimitConfig limitConfig,
@Nullable InitializeStats.Builder initStatsBuilder,
- @NonNull OptimizeStrategy optimizeStrategy)
+ @NonNull OptimizeStrategy optimizeStrategy,
+ @Nullable VisibilityChecker visibilityChecker)
throws AppSearchException {
Preconditions.checkNotNull(icingDir);
mLimitConfig = Preconditions.checkNotNull(limitConfig);
mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy);
+ mVisibilityCheckerLocked = visibilityChecker;
mReadWriteLock.writeLock().lock();
try {
@@ -355,6 +383,14 @@
resetLocked(initStatsBuilder);
}
+ long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime();
+ mVisibilityStoreLocked = new VisibilityStore(this);
+ long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime();
+ if (initStatsBuilder != null) {
+ initStatsBuilder.setPrepareVisibilityStoreLatencyMillis((int)
+ (prepareVisibilityStoreLatencyEndMillis
+ - prepareVisibilityStoreLatencyStartMillis));
+ }
} finally {
mReadWriteLock.writeLock().unlock();
}
@@ -397,21 +433,21 @@
*
* <p>This method belongs to mutate group.
*
- * @param packageName The package name that owns the schemas.
- * @param databaseName The name of the database where this schema lives.
- * @param schemas Schemas to set for this app.
- * @param visibilityStore If set, {@code schemasNotDisplayedBySystem} and {@code
- * schemasVisibleToPackages} will be saved here if the
- * schema is successfully applied.
- * @param schemasNotDisplayedBySystem Schema types that should not be surfaced on platform
- * surfaces.
- * @param schemasVisibleToPackages Schema types that are visible to the specified packages.
- * @param forceOverride Whether to force-apply the schema even if it is
- * incompatible. Documents
- * which do not comply with the new schema will be deleted.
- * @param version The overall version number of the request.
- * @param setSchemaStatsBuilder Builder for {@link SetSchemaStats} to hold stats for
- * setSchema
+ * @param packageName The package name that owns the schemas.
+ * @param databaseName The name of the database where this schema lives.
+ * @param schemas Schemas to set for this app.
+ * @param visibilityDocuments {@link VisibilityDocument}s that contain all
+ * visibility setting information for those schemas
+ * has user custom settings. Other schemas in the list
+ * that don't has a {@link VisibilityDocument}
+ * will be treated as having the default visibility,
+ * which is accessible by the system and no other packages.
+ * @param forceOverride Whether to force-apply the schema even if it is
+ * incompatible. Documents
+ * which do not comply with the new schema will be deleted.
+ * @param version The overall version number of the request.
+ * @param setSchemaStatsBuilder Builder for {@link SetSchemaStats} to hold stats for
+ * setSchema
* @return The response contains deleted schema types and incompatible schema types of this
* call.
* @throws AppSearchException On IcingSearchEngine error. If the status code is
@@ -423,160 +459,387 @@
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<AppSearchSchema> schemas,
- @Nullable VisibilityStore visibilityStore,
- @NonNull List<String> schemasNotDisplayedBySystem,
- @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
boolean forceOverride,
int version,
@Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
mReadWriteLock.writeLock().lock();
try {
throwIfClosedLocked();
-
- SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
-
- SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
- for (int i = 0; i < schemas.size(); i++) {
- AppSearchSchema schema = schemas.get(i);
- SchemaTypeConfigProto schemaTypeProto =
- SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
- newSchemaBuilder.addTypes(schemaTypeProto);
- }
-
- String prefix = createPrefix(packageName, databaseName);
- // Combine the existing schema (which may have types from other prefixes) with this
- // prefix's new schema. Modifies the existingSchemaBuilder.
- RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(prefix,
- existingSchemaBuilder,
- newSchemaBuilder.build());
-
- // Apply schema
- SchemaProto finalSchema = existingSchemaBuilder.build();
- mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
- SetSchemaResultProto setSchemaResultProto =
- mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
- mLogUtil.piiTrace(
- "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
-
- if (setSchemaStatsBuilder != null) {
- setSchemaStatsBuilder.setStatusCode(statusProtoToResultCode(
- setSchemaResultProto.getStatus()));
- AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto,
- setSchemaStatsBuilder);
- }
-
- // Determine whether it succeeded.
- try {
- checkSuccess(setSchemaResultProto.getStatus());
- } catch (AppSearchException e) {
- // Swallow the exception for the incompatible change case. We will propagate
- // those deleted schemas and incompatible types to the SetSchemaResponse.
- boolean isFailedPrecondition = setSchemaResultProto.getStatus().getCode()
- == StatusProto.Code.FAILED_PRECONDITION;
- boolean isIncompatible = setSchemaResultProto.getDeletedSchemaTypesCount() > 0
- || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
- if (isFailedPrecondition && isIncompatible) {
- return SetSchemaResponseToProtoConverter
- .toSetSchemaResponse(setSchemaResultProto, prefix);
- } else {
- throw e;
- }
- }
-
- // Update derived data structures.
- for (SchemaTypeConfigProto schemaTypeConfigProto :
- rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
- addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
- }
-
- for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
- removeFromMap(mSchemaMapLocked, prefix, schemaType);
- }
-
- if (visibilityStore != null) {
- Set<String> prefixedSchemasNotDisplayedBySystem =
- new ArraySet<>(schemasNotDisplayedBySystem.size());
- for (int i = 0; i < schemasNotDisplayedBySystem.size(); i++) {
- prefixedSchemasNotDisplayedBySystem.add(
- prefix + schemasNotDisplayedBySystem.get(i));
- }
-
- Map<String, List<PackageIdentifier>> prefixedSchemasVisibleToPackages =
- new ArrayMap<>(schemasVisibleToPackages.size());
- for (Map.Entry<String, List<PackageIdentifier>> entry
- : schemasVisibleToPackages.entrySet()) {
- prefixedSchemasVisibleToPackages.put(prefix + entry.getKey(), entry.getValue());
- }
-
- visibilityStore.setVisibility(
+ if (mObserverManager.isPackageObserved(packageName)) {
+ return doSetSchemaWithChangeNotificationLocked(
packageName,
databaseName,
- prefixedSchemasNotDisplayedBySystem,
- prefixedSchemasVisibleToPackages);
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
+ } else {
+ return doSetSchemaNoChangeNotificationLocked(
+ packageName,
+ databaseName,
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
}
-
- return SetSchemaResponseToProtoConverter
- .toSetSchemaResponse(setSchemaResultProto, prefix);
} finally {
mReadWriteLock.writeLock().unlock();
}
}
/**
+ * Updates the AppSearch schema for this app, dispatching change notifications.
+ *
+ * @see #setSchema
+ * @see #doSetSchemaNoChangeNotificationLocked
+ */
+ @GuardedBy("mReadWriteLock")
+ @NonNull
+ private SetSchemaResponse doSetSchemaWithChangeNotificationLocked(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull List<AppSearchSchema> schemas,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
+ boolean forceOverride,
+ int version,
+ @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
+ // First, capture the old state of the system. This includes the old schema as well as
+ // whether each registered observer can access each type. Once VisibilityStore is updated
+ // by the setSchema call, the information of which observers could see which types will be
+ // lost.
+ GetSchemaResponse oldSchema = getSchema(
+ packageName,
+ databaseName,
+ // A CallerAccess object for internal use that has local access to this database.
+ new CallerAccess(/*callingPackageName=*/packageName));
+
+ // Cache some lookup tables to help us work with the old schema
+ Set<AppSearchSchema> oldSchemaTypes = oldSchema.getSchemas();
+ Map<String, AppSearchSchema> oldSchemaNameToType = new ArrayMap<>(oldSchemaTypes.size());
+ // Maps unprefixed schema name to the set of listening packages that had visibility into
+ // that type under the old schema.
+ Map<String, Set<String>> oldSchemaNameToVisibleListeningPackage =
+ new ArrayMap<>(oldSchemaTypes.size());
+ for (AppSearchSchema oldSchemaType : oldSchemaTypes) {
+ String oldSchemaName = oldSchemaType.getSchemaType();
+ oldSchemaNameToType.put(oldSchemaName, oldSchemaType);
+ oldSchemaNameToVisibleListeningPackage.put(
+ oldSchemaName,
+ mObserverManager.getObserversForSchemaType(
+ packageName,
+ databaseName,
+ oldSchemaName,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked));
+ }
+
+ // Apply the new schema
+ SetSchemaResponse setSchemaResponse = doSetSchemaNoChangeNotificationLocked(
+ packageName,
+ databaseName,
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
+
+ // Cache some lookup tables to help us work with the new schema
+ Map<String, AppSearchSchema> newSchemaNameToType = new ArrayMap<>(schemas.size());
+ // Maps unprefixed schema name to the set of listening packages that have visibility into
+ // that type under the new schema.
+ Map<String, Set<String>> newSchemaNameToVisibleListeningPackage =
+ new ArrayMap<>(schemas.size());
+ for (AppSearchSchema newSchemaType : schemas) {
+ String newSchemaName = newSchemaType.getSchemaType();
+ newSchemaNameToType.put(newSchemaName, newSchemaType);
+ newSchemaNameToVisibleListeningPackage.put(
+ newSchemaName,
+ mObserverManager.getObserversForSchemaType(
+ packageName,
+ databaseName,
+ newSchemaName,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked));
+ }
+
+ // Create a unified set of all schema names mentioned in either the old or new schema.
+ Set<String> allSchemaNames = new ArraySet<>(oldSchemaNameToType.keySet());
+ allSchemaNames.addAll(newSchemaNameToType.keySet());
+
+ // Perform the diff between the old and new schema.
+ for (String schemaName : allSchemaNames) {
+ final AppSearchSchema contentBefore = oldSchemaNameToType.get(schemaName);
+ final AppSearchSchema contentAfter = newSchemaNameToType.get(schemaName);
+
+ final boolean existBefore = (contentBefore != null);
+ final boolean existAfter = (contentAfter != null);
+
+ // This should never happen
+ if (!existBefore && !existAfter) {
+ continue;
+ }
+
+ boolean contentsChanged = true;
+ if (existBefore && existAfter && contentBefore.equals(contentAfter)) {
+ contentsChanged = false;
+ }
+
+ Set<String> oldVisibleListeners =
+ oldSchemaNameToVisibleListeningPackage.get(schemaName);
+ Set<String> newVisibleListeners =
+ newSchemaNameToVisibleListeningPackage.get(schemaName);
+ Set<String> allListeningPackages = new ArraySet<>(oldVisibleListeners);
+ if (newVisibleListeners != null) {
+ allListeningPackages.addAll(newVisibleListeners);
+ }
+
+ // Now that we've computed the relationship between the old and new schema, we go
+ // observer by observer and consider the observer's own personal view of the schema.
+ for (String listeningPackageName : allListeningPackages) {
+ // Figure out the visibility
+ final boolean visibleBefore = (
+ existBefore
+ && oldVisibleListeners != null
+ && oldVisibleListeners.contains(listeningPackageName));
+ final boolean visibleAfter = (
+ existAfter
+ && newVisibleListeners != null
+ && newVisibleListeners.contains(listeningPackageName));
+
+ // Now go through the truth table of all the relevant flags.
+ // visibleBefore and visibleAfter take into account existBefore and existAfter, so
+ // we can stop worrying about existBefore and existAfter.
+ boolean sendNotification = false;
+ if (visibleBefore && visibleAfter && contentsChanged) {
+ sendNotification = true; // Type configuration was modified
+ } else if (!visibleBefore && visibleAfter) {
+ sendNotification = true; // Newly granted visibility or type was created
+ } else if (visibleBefore && !visibleAfter) {
+ sendNotification = true; // Revoked visibility or type was deleted
+ } else {
+ // No visibility before and no visibility after. Nothing to dispatch.
+ }
+
+ if (sendNotification) {
+ mObserverManager.onSchemaChange(
+ /*listeningPackageName=*/listeningPackageName,
+ /*targetPackageName=*/packageName,
+ /*databaseName=*/databaseName,
+ /*schemaName=*/schemaName);
+ }
+ }
+ }
+
+ return setSchemaResponse;
+ }
+
+ /**
+ * Updates the AppSearch schema for this app, without dispatching change notifications.
+ *
+ * <p>This method can be used only when no one is observing {@code packageName}.
+ *
+ * @see #setSchema
+ * @see #doSetSchemaWithChangeNotificationLocked
+ */
+ @GuardedBy("mReadWriteLock")
+ @NonNull
+ private SetSchemaResponse doSetSchemaNoChangeNotificationLocked(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull List<AppSearchSchema> schemas,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
+ boolean forceOverride,
+ int version,
+ @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
+ SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
+
+ SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
+ for (int i = 0; i < schemas.size(); i++) {
+ AppSearchSchema schema = schemas.get(i);
+ SchemaTypeConfigProto schemaTypeProto =
+ SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
+ newSchemaBuilder.addTypes(schemaTypeProto);
+ }
+
+ String prefix = createPrefix(packageName, databaseName);
+ // Combine the existing schema (which may have types from other prefixes) with this
+ // prefix's new schema. Modifies the existingSchemaBuilder.
+ RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(prefix,
+ existingSchemaBuilder,
+ newSchemaBuilder.build());
+
+ // Apply schema
+ SchemaProto finalSchema = existingSchemaBuilder.build();
+ mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
+ SetSchemaResultProto setSchemaResultProto =
+ mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
+ mLogUtil.piiTrace(
+ "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
+
+ if (setSchemaStatsBuilder != null) {
+ setSchemaStatsBuilder.setStatusCode(statusProtoToResultCode(
+ setSchemaResultProto.getStatus()));
+ AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto,
+ setSchemaStatsBuilder);
+ }
+
+ // Determine whether it succeeded.
+ try {
+ checkSuccess(setSchemaResultProto.getStatus());
+ } catch (AppSearchException e) {
+ // Swallow the exception for the incompatible change case. We will propagate
+ // those deleted schemas and incompatible types to the SetSchemaResponse.
+ boolean isFailedPrecondition = setSchemaResultProto.getStatus().getCode()
+ == StatusProto.Code.FAILED_PRECONDITION;
+ boolean isIncompatible = setSchemaResultProto.getDeletedSchemaTypesCount() > 0
+ || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
+ if (isFailedPrecondition && isIncompatible) {
+ return SetSchemaResponseToProtoConverter
+ .toSetSchemaResponse(setSchemaResultProto, prefix);
+ } else {
+ throw e;
+ }
+ }
+
+ // Update derived data structures.
+ for (SchemaTypeConfigProto schemaTypeConfigProto :
+ rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
+ addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+ }
+
+ for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
+ removeFromMap(mSchemaMapLocked, prefix, schemaType);
+ }
+ // Since the constructor of VisibilityStore will set schema. Avoid call visibility
+ // store before we have already created it.
+ if (mVisibilityStoreLocked != null) {
+ // Add prefix to all visibility documents.
+ List<VisibilityDocument> prefixedVisibilityDocuments =
+ new ArrayList<>(visibilityDocuments.size());
+ // Find out which Visibility document is deleted or changed to all-default settings.
+ // We need to remove them from Visibility Store.
+ Set<String> deprecatedVisibilityDocuments =
+ new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet());
+ for (int i = 0; i < visibilityDocuments.size(); i++) {
+ VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i);
+ // The VisibilityDocument is controlled by the client and it's untrusted but we
+ // make it safe by appending a prefix.
+ // We must control the package-database prefix. Therefore even if the client
+ // fake the id, they can only mess their own app. That's totally allowed and
+ // they can do this via the public API too.
+ String prefixedSchemaType = prefix + unPrefixedDocument.getId();
+ prefixedVisibilityDocuments.add(new VisibilityDocument(
+ unPrefixedDocument.toBuilder()
+ .setId(prefixedSchemaType)
+ .build()));
+ // This schema has visibility settings. We should keep it from the removal list.
+ deprecatedVisibilityDocuments.remove(prefixedSchemaType);
+ }
+ // Now deprecatedVisibilityDocuments contains those existing schemas that has
+ // all-default visibility settings, add deleted schemas. That's all we need to
+ // remove.
+ deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes);
+ mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments);
+ mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments);
+ }
+ return SetSchemaResponseToProtoConverter
+ .toSetSchemaResponse(setSchemaResultProto, prefix);
+ }
+
+ /**
* Retrieves the AppSearch schema for this package name, database.
*
* <p>This method belongs to query group.
*
- * @param packageName Package name that owns this schema
- * @param databaseName The name of the database where this schema lives.
+ * @param packageName Package that owns the requested {@link AppSearchSchema} instances.
+ * @param databaseName Database that owns the requested {@link AppSearchSchema} instances.
+ * @param callerAccess Visibility access info of the calling app
* @throws AppSearchException on IcingSearchEngine error.
*/
@NonNull
- public GetSchemaResponse getSchema(@NonNull String packageName,
- @NonNull String databaseName) throws AppSearchException {
+ public GetSchemaResponse getSchema(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull CallerAccess callerAccess)
+ throws AppSearchException {
mReadWriteLock.readLock().lock();
try {
throwIfClosedLocked();
SchemaProto fullSchema = getSchemaProtoLocked();
-
String prefix = createPrefix(packageName, databaseName);
GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder();
for (int i = 0; i < fullSchema.getTypesCount(); i++) {
- String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
+ // Check that this type belongs to the requested app and that the caller has
+ // access to it.
+ SchemaTypeConfigProto typeConfig = fullSchema.getTypes(i);
+ String prefixedSchemaType = typeConfig.getSchemaType();
+ String typePrefix = getPrefix(prefixedSchemaType);
if (!prefix.equals(typePrefix)) {
+ // This schema type doesn't belong to the database we're querying for.
continue;
}
- // Rewrite SchemaProto.types.schema_type
- SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(
- i).toBuilder();
- String newSchemaType =
- typeConfigBuilder.getSchemaType().substring(prefix.length());
- typeConfigBuilder.setSchemaType(newSchemaType);
-
- // Rewrite SchemaProto.types.properties.schema_type
- for (int propertyIdx = 0;
- propertyIdx < typeConfigBuilder.getPropertiesCount();
- propertyIdx++) {
- PropertyConfigProto.Builder propertyConfigBuilder =
- typeConfigBuilder.getProperties(propertyIdx).toBuilder();
- if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
- String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
- .substring(prefix.length());
- propertyConfigBuilder.setSchemaType(newPropertySchemaType);
- typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
- }
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ callerAccess,
+ packageName,
+ prefixedSchemaType,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked)) {
+ // Caller doesn't have access to this type.
+ continue;
}
+ // Rewrite SchemaProto.types.schema_type
+ SchemaTypeConfigProto.Builder typeConfigBuilder = typeConfig.toBuilder();
+ PrefixUtil.removePrefixesFromSchemaType(typeConfigBuilder);
AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(
typeConfigBuilder);
//TODO(b/183050495) find a place to store the version for the database, rather
// than read from a schema.
- responseBuilder.setVersion(fullSchema.getTypes(i).getVersion());
+ responseBuilder.setVersion(typeConfig.getVersion());
responseBuilder.addSchema(schema);
+
+ // Populate visibility info. Since the constructor of VisibilityStore will get
+ // schema. Avoid call visibility store before we have already created it.
+ if (mVisibilityStoreLocked != null) {
+ String typeName = typeConfig.getSchemaType().substring(typePrefix.length());
+ VisibilityDocument visibilityDocument =
+ mVisibilityStoreLocked.getVisibility(prefixedSchemaType);
+ if (visibilityDocument != null) {
+ if (visibilityDocument.isNotDisplayedBySystem()) {
+ responseBuilder
+ .addSchemaTypeNotDisplayedBySystem(typeName);
+ }
+ String[] packageNames = visibilityDocument.getPackageNames();
+ byte[][] sha256Certs = visibilityDocument.getSha256Certs();
+ if (packageNames.length != sha256Certs.length) {
+ throw new AppSearchException(RESULT_INTERNAL_ERROR,
+ "The length of package names and sha256Crets are different!");
+ }
+ if (packageNames.length != 0) {
+ Set<PackageIdentifier> packageIdentifier = new ArraySet<>();
+ for (int j = 0; j < packageNames.length; j++) {
+ packageIdentifier.add(new PackageIdentifier(
+ packageNames[j], sha256Certs[j]));
+ }
+ responseBuilder.setSchemaTypeVisibleToPackages(typeName,
+ packageIdentifier);
+ }
+ Set<Set<Integer>> visibleToPermissions =
+ visibilityDocument.getVisibleToPermissions();
+ if (visibleToPermissions != null) {
+ responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(
+ typeName, visibleToPermissions);
+ }
+ }
+ }
}
return responseBuilder.build();
+
} finally {
mReadWriteLock.readLock().unlock();
}
@@ -689,7 +952,13 @@
// Prepare notifications
mObserverManager.onDocumentChange(
- packageName, databaseName, document.getNamespace(), document.getSchemaType());
+ packageName,
+ databaseName,
+ document.getNamespace(),
+ document.getSchemaType(),
+ document.getId(),
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked);
} finally {
mReadWriteLock.writeLock().unlock();
@@ -758,6 +1027,67 @@
}
/**
+ * Retrieves a document from the AppSearch index by namespace and document ID from any
+ * application the caller is allowed to view
+ *
+ * <p>This method will handle both Icing engine errors as well as permission errors by
+ * throwing an obfuscated RESULT_NOT_FOUND exception. This is done so the caller doesn't
+ * receive information on whether or not a file they are not allowed to access exists or not.
+ * This is different from the behavior of {@link #getDocument}.
+ *
+ * @param packageName The package that owns this document.
+ * @param databaseName The databaseName this document resides in.
+ * @param namespace The namespace this document resides in.
+ * @param id The ID of the document to get.
+ * @param typePropertyPaths A map of schema type to a list of property paths to return in the
+ * result.
+ * @param callerAccess Visibility access info of the calling app
+ * @return The Document contents
+ * @throws AppSearchException on IcingSearchEngine error or invalid permissions
+ */
+ @NonNull
+ public GenericDocument globalGetDocument(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String namespace,
+ @NonNull String id,
+ @NonNull Map<String, List<String>> typePropertyPaths,
+ @NonNull CallerAccess callerAccess) throws AppSearchException {
+ mReadWriteLock.readLock().lock();
+ try {
+ throwIfClosedLocked();
+ // We retrieve the document before checking for access, as we do not know which
+ // schema the document is under. Schema is required for checking access
+ DocumentProto documentProto;
+ try {
+ documentProto = getDocumentProtoByIdLocked(packageName, databaseName,
+ namespace, id, typePropertyPaths);
+
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ callerAccess,
+ packageName,
+ documentProto.getSchema(),
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked)) {
+ throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND);
+ }
+ } catch (AppSearchException e) {
+ throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
+ "Document (" + namespace + ", " + id + ") not found.");
+ }
+
+ DocumentProto.Builder documentBuilder = documentProto.toBuilder();
+ removePrefixesFromDocument(documentBuilder);
+ String prefix = createPrefix(packageName, databaseName);
+ Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
+ return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
+ prefix, schemaTypeMap);
+ } finally {
+ mReadWriteLock.readLock().unlock();
+ }
+ }
+
+ /**
* Retrieves a document from the AppSearch index by namespace and document ID.
*
* <p>This method belongs to query group.
@@ -773,47 +1103,24 @@
*/
@NonNull
public GenericDocument getDocument(
- @NonNull String packageName, @NonNull String databaseName,
+ @NonNull String packageName,
+ @NonNull String databaseName,
@NonNull String namespace,
@NonNull String id,
@NonNull Map<String, List<String>> typePropertyPaths) throws AppSearchException {
mReadWriteLock.readLock().lock();
try {
throwIfClosedLocked();
+ DocumentProto documentProto = getDocumentProtoByIdLocked(packageName, databaseName,
+ namespace, id, typePropertyPaths);
+ DocumentProto.Builder documentBuilder = documentProto.toBuilder();
+ removePrefixesFromDocument(documentBuilder);
+
String prefix = createPrefix(packageName, databaseName);
- List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
- TypePropertyPathToProtoConverter
- .toTypePropertyMaskBuilderList(typePropertyPaths);
- List<TypePropertyMask> prefixedPropertyMasks =
- new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
- for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
- String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
- String prefixedType = nonPrefixedType.equals(
- GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
- ? nonPrefixedType : prefix + nonPrefixedType;
- prefixedPropertyMasks.add(
- nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build());
- }
- GetResultSpecProto getResultSpec =
- GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks)
- .build();
-
- String finalNamespace = createPrefix(packageName, databaseName) + namespace;
- if (mLogUtil.isPiiTraceEnabled()) {
- mLogUtil.piiTrace(
- "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec);
- }
- GetResultProto getResultProto =
- mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec);
- mLogUtil.piiTrace("getDocument, response", getResultProto.getStatus(), getResultProto);
- checkSuccess(getResultProto.getStatus());
-
// The schema type map cannot be null at this point. It could only be null if no
// schema had ever been set for that prefix. Given we have retrieved a document from
// the index, we know a schema had to have been set.
Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
- DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
- removePrefixesFromDocument(documentBuilder);
return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
prefix, schemaTypeMap);
} finally {
@@ -822,6 +1129,58 @@
}
/**
+ * Returns a DocumentProto from Icing.
+ *
+ * @param packageName The package that owns this document.
+ * @param databaseName The databaseName this document resides in.
+ * @param namespace The namespace this document resides in.
+ * @param id The ID of the document to get.
+ * @param typePropertyPaths A map of schema type to a list of property paths to return in the
+ * result.
+ * @return the DocumentProto object
+ * @throws AppSearchException on IcingSearchEngine error
+ */
+ @NonNull
+ @GuardedBy("mReadWriteLock")
+ private DocumentProto getDocumentProtoByIdLocked(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String namespace,
+ @NonNull String id,
+ @NonNull Map<String, List<String>> typePropertyPaths)
+ throws AppSearchException {
+ String prefix = createPrefix(packageName, databaseName);
+ List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
+ TypePropertyPathToProtoConverter
+ .toTypePropertyMaskBuilderList(typePropertyPaths);
+ List<TypePropertyMask> prefixedPropertyMasks =
+ new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
+ for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
+ String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
+ String prefixedType = nonPrefixedType.equals(
+ GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
+ ? nonPrefixedType : prefix + nonPrefixedType;
+ prefixedPropertyMasks.add(
+ nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build());
+ }
+ GetResultSpecProto getResultSpec =
+ GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks)
+ .build();
+
+ String finalNamespace = createPrefix(packageName, databaseName) + namespace;
+ if (mLogUtil.isPiiTraceEnabled()) {
+ mLogUtil.piiTrace(
+ "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec);
+ }
+ GetResultProto getResultProto =
+ mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec);
+ mLogUtil.piiTrace("getDocument, response", getResultProto.getStatus(), getResultProto);
+ checkSuccess(getResultProto.getStatus());
+
+ return getResultProto.getDocument();
+ }
+
+ /**
* Executes a query against the AppSearch index and returns results.
*
* <p>This method belongs to query group.
@@ -897,16 +1256,10 @@
*
* <p>This method belongs to query group.
*
- * @param queryExpression Query String to search.
- * @param searchSpec Spec for setting filters, raw query etc.
- * @param callerPackageName Package name of the caller, should belong to the {@code
- * callerUserHandle}.
- * @param visibilityStore Optional visibility store to obtain system and package
- * visibility settings from
- * @param callerUid UID of the client making the globalQuery call.
- * @param callerHasSystemAccess Whether the caller has been positively identified as having
- * access to schemas marked system surfaceable.
- * @param logger logger to collect globalQuery stats
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec for setting filters, raw query etc.
+ * @param callerAccess Visibility access info of the calling app
+ * @param logger logger to collect globalQuery stats
* @return The results of performing this search. It may contain an empty list of results if
* no documents matched the query.
* @throws AppSearchException on IcingSearchEngine error.
@@ -915,10 +1268,7 @@
public SearchResultPage globalQuery(
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec,
- @NonNull String callerPackageName,
- @Nullable VisibilityStore visibilityStore,
- int callerUid,
- boolean callerHasSystemAccess,
+ @NonNull CallerAccess callerAccess,
@Nullable AppSearchLogger logger) throws AppSearchException {
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
SearchStats.Builder sStatsBuilder = null;
@@ -926,7 +1276,7 @@
sStatsBuilder =
new SearchStats.Builder(
SearchStats.VISIBILITY_SCOPE_GLOBAL,
- callerPackageName);
+ callerAccess.getCallingPackageName());
}
mReadWriteLock.readLock().lock();
@@ -952,10 +1302,10 @@
}
SearchSpecToProtoConverter searchSpecToProtoConverter =
new SearchSpecToProtoConverter(searchSpec, prefixFilters, mNamespaceMapLocked,
- mSchemaMapLocked);
+ mSchemaMapLocked);
// Remove those inaccessible schemas.
- searchSpecToProtoConverter.removeInaccessibleSchemaFilter(callerPackageName,
- visibilityStore, callerUid, callerHasSystemAccess);
+ searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
+ callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
if (searchSpecToProtoConverter.isNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return an
// empty SearchResult and skip sending request to Icing.
@@ -966,7 +1316,8 @@
queryExpression,
searchSpecToProtoConverter,
sStatsBuilder);
- addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
+ addNextPageToken(
+ callerAccess.getCallingPackageName(), searchResultPage.getNextPageToken());
return searchResultPage;
} finally {
mReadWriteLock.readLock().unlock();
@@ -1266,7 +1617,14 @@
// Prepare notifications
if (schemaType != null) {
- mObserverManager.onDocumentChange(packageName, databaseName, namespace, schemaType);
+ mObserverManager.onDocumentChange(
+ packageName,
+ databaseName,
+ namespace,
+ schemaType,
+ documentId,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked);
}
} finally {
mReadWriteLock.writeLock().unlock();
@@ -1360,9 +1718,9 @@
/**
* Executes removeByQuery, creating change notifications for removal.
*
- * @param packageName The package name that owns the documents.
- * @param finalSearchSpec The final search spec that has been written through
- * {@link SearchSpecToProtoConverter}.
+ * @param packageName The package name that owns the documents.
+ * @param finalSearchSpec The final search spec that has been written through
+ * {@link SearchSpecToProtoConverter}.
* @param prefixedObservedSchemas The set of prefixed schemas that have valid registered
* observers. Only changes to schemas in this set will be queued.
*/
@@ -1427,7 +1785,10 @@
packageName,
/*databaseName=*/ PrefixUtil.getDatabaseName(document.getNamespace()),
/*namespace=*/ PrefixUtil.removePrefix(document.getNamespace()),
- /*schemaType=*/ PrefixUtil.removePrefix(document.getSchema()));
+ /*schemaType=*/ PrefixUtil.removePrefix(document.getSchema()),
+ document.getUri(),
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked);
}
}
@@ -1771,12 +2132,13 @@
}
for (String databaseName : databaseNames) {
String removedPrefix = createPrefix(packageName, databaseName);
- mSchemaMapLocked.remove(removedPrefix);
+ Map<String, SchemaTypeConfigProto> removedSchemas =
+ mSchemaMapLocked.remove(removedPrefix);
+ mVisibilityStoreLocked.removeVisibility(removedSchemas.keySet());
mNamespaceMapLocked.remove(removedPrefix);
}
}
}
- //TODO(b/145759910) clear visibility setting for package.
} finally {
mReadWriteLock.writeLock().unlock();
}
@@ -1951,7 +2313,7 @@
synchronized (mNextPageTokensLocked) {
Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName);
if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) {
- throw new AppSearchException(AppSearchResult.RESULT_SECURITY_ERROR,
+ throw new AppSearchException(RESULT_SECURITY_ERROR,
"Package \"" + packageName + "\" cannot use nextPageToken: "
+ nextPageToken);
}
@@ -1959,48 +2321,60 @@
}
/**
- * Adds an {@link AppSearchObserverCallback} to monitor changes within the
- * databases owned by {@code observedPackage} if they match the given
+ * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+ * {@code targetPackageName} if they match the given
* {@link androidx.appsearch.observer.ObserverSpec}.
*
- * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} changes its schema visibility settings.
+ * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} changes its schema visibility settings.
*
- * <p>If no package matching {@code observedPackage} exists on the system, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} is installed and starts indexing data.
+ * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} is installed and starts indexing data.
*
* <p>Note that this method does not take the standard read/write lock that guards I/O, so it
* will not queue behind I/O. Therefore it is safe to call from any thread including UI or
* binder threads.
+ *
+ * @param listeningPackageAccess Visibility information about the app that wants to receive
+ * notifications.
+ * @param targetPackageName The package that owns the data the observer wants to be
+ * notified for.
+ * @param spec Describes the kind of data changes the observer should trigger
+ * for.
+ * @param executor The executor on which to trigger the observer callback to
+ * deliver notifications.
+ * @param observer The callback to trigger on notifications.
*/
- public void addObserver(
- @NonNull String observedPackage,
+ public void registerObserverCallback(
+ @NonNull CallerAccess listeningPackageAccess,
+ @NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer) {
+ @NonNull ObserverCallback observer) {
// This method doesn't consult mSchemaMap or mNamespaceMap, and it will register
// observers for types that don't exist. This is intentional because we notify for types
// being created or removed. If we only registered observer for existing types, it would
// be impossible to ever dispatch a notification of a type being added.
- mObserverManager.addObserver(observedPackage, spec, executor, observer);
+ mObserverManager.registerObserverCallback(
+ listeningPackageAccess, targetPackageName, spec, executor, observer);
}
/**
- * Removes an {@link AppSearchObserverCallback} from watching the databases owned by
- * {@code observedPackage}.
+ * Removes an {@link ObserverCallback} from watching the databases owned by
+ * {@code targetPackageName}.
*
* <p>All observers which compare equal to the given observer via
- * {@link AppSearchObserverCallback#equals} are removed. This may be 0, 1, or many observers.
+ * {@link ObserverCallback#equals} are removed. This may be 0, 1, or many observers.
*
* <p>Note that this method does not take the standard read/write lock that guards I/O, so it
* will not queue behind I/O. Therefore it is safe to call from any thread including UI or
* binder threads.
*/
- public void removeObserver(
- @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
- mObserverManager.removeObserver(observedPackage, observer);
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
+ mObserverManager.unregisterObserverCallback(targetPackageName, observer);
}
/**
@@ -2166,6 +2540,25 @@
}
/**
+ * Returns all prefixed schema types saved in AppSearch.
+ *
+ * <p>This method is inefficient to call repeatedly.
+ */
+ @NonNull
+ public List<String> getAllPrefixedSchemaTypes() {
+ mReadWriteLock.readLock().lock();
+ try {
+ List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
+ for (Map<String, SchemaTypeConfigProto> value : mSchemaMapLocked.values()) {
+ cachedPrefixedSchemaTypes.addAll(value.keySet());
+ }
+ return cachedPrefixedSchemaTypes;
+ } finally {
+ mReadWriteLock.readLock().unlock();
+ }
+ }
+
+ /**
* Converts an erroneous status code from the Icing status enums to the AppSearchResult enums.
*
* <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
index 393f8f2..fe7bc77 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
@@ -165,7 +165,7 @@
* {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}
* added in.
* @return the {@link SetSchemaResponse} for this
- * {@link androidx.appsearch.app.AppSearchSession#setSchema} call.
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync} call.
*
* @throws IOException on i/o problem
* @throws AppSearchException on AppSearch problem
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index 6c9e456..ef8ec3b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -16,24 +16,34 @@
// @exportToFramework:skipFile()
package androidx.appsearch.localstorage;
+import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
+
+import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.app.ReportSystemUsageRequest;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.util.FutureUtil;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -48,12 +58,12 @@
private final Executor mExecutor;
private final Features mFeatures;
private final Context mContext;
+ @Nullable private final AppSearchLogger mLogger;
+
+ private final CallerAccess mSelfCallerAccess;
private boolean mIsClosed = false;
- @Nullable
- private final AppSearchLogger mLogger;
-
GlobalSearchSessionImpl(
@NonNull AppSearchImpl appSearchImpl,
@NonNull Executor executor,
@@ -65,6 +75,37 @@
mFeatures = Preconditions.checkNotNull(features);
mContext = Preconditions.checkNotNull(context);
mLogger = logger;
+
+ mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mContext.getPackageName());
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(databaseName);
+ Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ return FutureUtil.execute(mExecutor, () -> {
+ AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
+ new AppSearchBatchResult.Builder<>();
+
+ Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+ CallerAccess access = new CallerAccess(mContext.getPackageName());
+ for (String id : request.getIds()) {
+ try {
+ GenericDocument document = mAppSearchImpl.globalGetDocument(packageName,
+ databaseName, request.getNamespace(), id, typePropertyPaths, access);
+ resultBuilder.setSuccess(id, document);
+ } catch (Throwable t) {
+ resultBuilder.setResult(id, throwableToFailedResult(t));
+ }
+ }
+ return resultBuilder.build();
+ });
}
@NonNull
@@ -92,7 +133,8 @@
*/
@NonNull
@Override
- public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+ public ListenableFuture<Void> reportSystemUsageAsync(
+ @NonNull ReportSystemUsageRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
return FutureUtil.execute(mExecutor, () -> {
@@ -102,6 +144,18 @@
});
}
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync(
+ @NonNull String packageName, @NonNull String databaseName) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(databaseName);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+ return FutureUtil.execute(mExecutor,
+ () -> mAppSearchImpl.getSchema(packageName, databaseName, mSelfCallerAccess));
+ }
+
@NonNull
@Override
public Features getFeatures() {
@@ -109,36 +163,41 @@
}
@Override
- public void addObserver(
- @NonNull String observedPackage,
+ public void registerObserverCallback(
+ @NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer) {
- Preconditions.checkNotNull(observedPackage);
+ @NonNull ObserverCallback observer) {
+ Preconditions.checkNotNull(targetPackageName);
Preconditions.checkNotNull(spec);
Preconditions.checkNotNull(executor);
Preconditions.checkNotNull(observer);
// LocalStorage does not support observing data from other packages.
- if (!observedPackage.equals(mContext.getPackageName())) {
+ if (!targetPackageName.equals(mContext.getPackageName())) {
throw new UnsupportedOperationException(
"Local storage implementation does not support receiving change notifications "
+ "from other packages.");
}
- mAppSearchImpl.addObserver(observedPackage, spec, executor, observer);
+ mAppSearchImpl.registerObserverCallback(
+ /*listeningPackageAccess=*/mSelfCallerAccess,
+ /*targetPackageName=*/targetPackageName,
+ spec,
+ executor,
+ observer);
}
@Override
- public void removeObserver(
- @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
- Preconditions.checkNotNull(observedPackage);
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
+ Preconditions.checkNotNull(targetPackageName);
Preconditions.checkNotNull(observer);
// LocalStorage does not support observing data from other packages.
- if (!observedPackage.equals(mContext.getPackageName())) {
+ if (!targetPackageName.equals(mContext.getPackageName())) {
throw new UnsupportedOperationException(
"Local storage implementation does not support receiving change notifications "
+ "from other packages.");
}
- mAppSearchImpl.removeObserver(observedPackage, observer);
+ mAppSearchImpl.unregisterObserverCallback(targetPackageName, observer);
}
@Override
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 3ffb93d..6935b5e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -270,7 +270,7 @@
* {@link AppSearchSession}
*/
@NonNull
- public static ListenableFuture<AppSearchSession> createSearchSession(
+ public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull SearchContext context) {
Preconditions.checkNotNull(context);
return FutureUtil.execute(context.mExecutor, () -> {
@@ -281,6 +281,18 @@
}
/**
+ * @deprecated use {@link #createSearchSessionAsync}
+ * @param context The {@link SearchContext} contains all information to create a new
+ * {@link AppSearchSession}
+ */
+ @NonNull
+ @Deprecated
+ public static ListenableFuture<AppSearchSession> createSearchSession(
+ @NonNull SearchContext context) {
+ return createSearchSessionAsync(context);
+ }
+
+ /**
* Opens a new {@link GlobalSearchSession} on this storage.
*
* <p>This process requires a native search library. If it's not created, the initialization
@@ -290,7 +302,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+ public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
@NonNull GlobalSearchContext context) {
Preconditions.checkNotNull(context);
return FutureUtil.execute(context.mExecutor, () -> {
@@ -301,6 +313,18 @@
}
/**
+ * @deprecated use {@link #createGlobalSearchSessionAsync}
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ @Deprecated
+ public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+ @NonNull GlobalSearchContext context) {
+ return createGlobalSearchSessionAsync(context);
+ }
+
+ /**
* Returns the singleton instance of {@link LocalStorage}.
*
* <p>If the system is not initialized, it will be initialized using the provided
@@ -342,7 +366,8 @@
icingDir,
new UnlimitedLimitConfig(),
initStatsBuilder,
- new JetpackOptimizeStrategy());
+ new JetpackOptimizeStrategy(),
+ /*visibilityChecker=*/null);
if (logger != null) {
initStatsBuilder.setTotalLatencyMillis(
@@ -382,7 +407,7 @@
mAppSearchImpl,
context.mExecutor,
new AlwaysSupportedFeatures(),
- context.mContext.getPackageName(),
+ context.mContext,
context.mDatabaseName,
context.mLogger);
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
index 0d49147..c25be25 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
@@ -22,22 +22,29 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
/**
- * Manages {@link AppSearchObserverCallback} instances and queues notifications to them for later
+ * Manages {@link ObserverCallback} instances and queues notifications to them for later
* dispatch.
*
* <p>This class is thread-safe.
@@ -84,72 +91,94 @@
}
private static final class ObserverInfo {
+ /** The package which registered the observer. */
+ final CallerAccess mListeningPackageAccess;
final ObserverSpec mObserverSpec;
final Executor mExecutor;
- final AppSearchObserverCallback mObserver;
- volatile Set<DocumentChangeGroupKey> mDocumentChanges = new ArraySet<>();
+ final ObserverCallback mObserverCallback;
+ // Values is a set of document IDs
+ volatile Map<DocumentChangeGroupKey, Set<String>> mDocumentChanges = new ArrayMap<>();
+ // Keys are database prefixes, values are a set of schema names
+ volatile Map<String, Set<String>> mSchemaChanges = new ArrayMap<>();
ObserverInfo(
+ @NonNull CallerAccess listeningPackageAccess,
@NonNull ObserverSpec observerSpec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer) {
+ @NonNull ObserverCallback observerCallback) {
+ mListeningPackageAccess = Preconditions.checkNotNull(listeningPackageAccess);
mObserverSpec = Preconditions.checkNotNull(observerSpec);
mExecutor = Preconditions.checkNotNull(executor);
- mObserver = Preconditions.checkNotNull(observer);
+ mObserverCallback = Preconditions.checkNotNull(observerCallback);
}
}
private final Object mLock = new Object();
- /** Maps observed package to observer infos watching something in that package. */
+ /** Maps target packages to ObserverInfos watching something in that package. */
@GuardedBy("mLock")
private final Map<String, List<ObserverInfo>> mObserversLocked = new ArrayMap<>();
private volatile boolean mHasNotifications = false;
/**
- * Adds an {@link AppSearchObserverCallback} to monitor changes within the
- * databases owned by {@code observedPackage} if they match the given
+ * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+ * {@code targetPackageName} if they match the given
* {@link androidx.appsearch.observer.ObserverSpec}.
*
- * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} changes its schema visibility settings.
+ * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} changes its schema visibility settings.
*
- * <p>If no package matching {@code observedPackage} exists on the system, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} is installed and starts indexing data.
+ * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} is installed and starts indexing data.
+ *
+ * <p>Note that this method does not take the standard read/write lock that guards I/O, so it
+ * will not queue behind I/O. Therefore it is safe to call from any thread including UI or
+ * binder threads.
+ *
+ * @param listeningPackageAccess Visibility information about the app that wants to receive
+ * notifications.
+ * @param targetPackageName The package that owns the data the observerCallback wants to be
+ * notified for.
+ * @param spec Describes the kind of data changes the observerCallback should
+ * trigger for.
+ * @param executor The executor on which to trigger the observerCallback callback
+ * to deliver notifications.
+ * @param observerCallback The callback to trigger on notifications.
*/
- public void addObserver(
- @NonNull String observedPackage,
+ public void registerObserverCallback(
+ @NonNull CallerAccess listeningPackageAccess,
+ @NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer) {
+ @NonNull ObserverCallback observerCallback) {
synchronized (mLock) {
- List<ObserverInfo> infos = mObserversLocked.get(observedPackage);
+ List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
if (infos == null) {
infos = new ArrayList<>();
- mObserversLocked.put(observedPackage, infos);
+ mObserversLocked.put(targetPackageName, infos);
}
- infos.add(new ObserverInfo(spec, executor, observer));
+ infos.add(new ObserverInfo(listeningPackageAccess, spec, executor, observerCallback));
}
}
/**
- * Removes all observers that match via {@link AppSearchObserverCallback#equals} to the given
- * observer from watching the observedPackage.
+ * Removes all observers that match via {@link ObserverCallback#equals} to the given observer
+ * from watching the targetPackageName.
*
* <p>Pending notifications queued for this observer, if any, are discarded.
*/
- public void removeObserver(
- @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
synchronized (mLock) {
- List<ObserverInfo> infos = mObserversLocked.get(observedPackage);
+ List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
if (infos == null) {
return;
}
for (int i = 0; i < infos.size(); i++) {
- if (infos.get(i).mObserver.equals(observer)) {
+ if (infos.get(i).mObserverCallback.equals(observer)) {
infos.remove(i);
i--;
}
@@ -162,28 +191,105 @@
*
* <p>The notification will be queued in memory for later dispatch. You must call
* {@link #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
+ *
+ * @param visibilityStore Store for visibility information. If not provided, only access to
+ * own data will be allowed.
+ * @param visibilityChecker Checker for visibility access. If not provided, only access to own
+ * data will be allowed.
*/
public void onDocumentChange(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String namespace,
- @NonNull String schemaType) {
+ @NonNull String schemaType,
+ @NonNull String documentId,
+ @Nullable VisibilityStore visibilityStore,
+ @Nullable VisibilityChecker visibilityChecker) {
synchronized (mLock) {
List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
return; // No observers for this type
}
// Enqueue changes for later dispatch once the call returns
+ String prefixedSchema =
+ PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
DocumentChangeGroupKey key = null;
for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
- if (matchesSpec(schemaType, observerInfo.mObserverSpec)) {
- if (key == null) {
- key = new DocumentChangeGroupKey(
- packageName, databaseName, namespace, schemaType);
- }
- observerInfo.mDocumentChanges.add(key);
+ if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
+ continue; // Observer doesn't want this notification
}
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ /*callerAccess=*/observerInfo.mListeningPackageAccess,
+ /*targetPackageName=*/packageName,
+ /*prefixedSchema=*/prefixedSchema,
+ visibilityStore,
+ visibilityChecker)) {
+ continue; // Observer can't have this notification.
+ }
+ if (key == null) {
+ key = new DocumentChangeGroupKey(
+ packageName, databaseName, namespace, schemaType);
+ }
+ Set<String> changedDocumentIds = observerInfo.mDocumentChanges.get(key);
+ if (changedDocumentIds == null) {
+ changedDocumentIds = new ArraySet<>();
+ observerInfo.mDocumentChanges.put(key, changedDocumentIds);
+ }
+ changedDocumentIds.add(documentId);
+ }
+ mHasNotifications = true;
+ }
+ }
+
+ /**
+ * Enqueues a change to a schema type for a single observer.
+ *
+ * <p>The notification will be queued in memory for later dispatch. You must call
+ * {@link #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
+ *
+ * <p>Note that unlike {@link #onDocumentChange}, the changes reported here are not dropped
+ * for observers that don't have visibility. This is because the observer might have had
+ * visibility before the schema change, and a final deletion needs to be sent to it. Caller
+ * is responsible for checking visibility of these notifications.
+ *
+ * @param listeningPackageName Name of package that subscribed to notifications and has been
+ * validated by the caller to have the right access to receive
+ * this notification.
+ * @param targetPackageName Name of package that owns the changed schema types.
+ * @param databaseName Database in which the changed schema types reside.
+ * @param schemaName Unprefixed name of the changed schema type.
+ */
+ public void onSchemaChange(
+ @NonNull String listeningPackageName,
+ @NonNull String targetPackageName,
+ @NonNull String databaseName,
+ @NonNull String schemaName) {
+ synchronized (mLock) {
+ List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(targetPackageName);
+ if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
+ return; // No observers for this type
+ }
+ // Enqueue changes for later dispatch once the call returns
+ String prefix = null;
+ for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+ ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+ if (!observerInfo.mListeningPackageAccess.getCallingPackageName()
+ .equals(listeningPackageName)) {
+ continue; // Not the observer we've been requested to update right now.
+ }
+ if (!matchesSpec(schemaName, observerInfo.mObserverSpec)) {
+ continue; // Observer doesn't want this notification
+ }
+ if (prefix == null) {
+ prefix = PrefixUtil.createPrefix(targetPackageName, databaseName);
+ }
+ Set<String> changedSchemaNames = observerInfo.mSchemaChanges.get(prefix);
+ if (changedSchemaNames == null) {
+ changedSchemaNames = new ArraySet<>();
+ observerInfo.mSchemaChanges.put(prefix, changedSchemaNames);
+ }
+ changedSchemaNames.add(schemaName);
}
mHasNotifications = true;
}
@@ -216,6 +322,44 @@
}
}
+ /**
+ * Returns package names of listening packages registered for changes on the given
+ * {@code packageName}, {@code databaseName} and unprefixed {@code schemaType}, only if they
+ * have access to that type according to the provided {@code visibilityChecker}.
+ */
+ @NonNull
+ public Set<String> getObserversForSchemaType(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String schemaType,
+ @Nullable VisibilityStore visibilityStore,
+ @Nullable VisibilityChecker visibilityChecker) {
+ synchronized (mLock) {
+ List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
+ if (allObserverInfosForPackage == null) {
+ return Collections.emptySet();
+ }
+ Set<String> result = new ArraySet<>();
+ String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
+ for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+ ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+ if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
+ continue; // Observer doesn't want this notification
+ }
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ /*callerAccess=*/observerInfo.mListeningPackageAccess,
+ /*targetPackageName=*/packageName,
+ /*prefixedSchema=*/prefixedSchema,
+ visibilityStore,
+ visibilityChecker)) {
+ continue; // Observer can't have this notification.
+ }
+ result.add(observerInfo.mListeningPackageAccess.getCallingPackageName());
+ }
+ return result;
+ }
+ }
+
/** Returns whether any notifications have been queued for dispatch. */
public boolean hasNotifications() {
return mHasNotifications;
@@ -243,28 +387,52 @@
@GuardedBy("mLock")
private void dispatchAndClearPendingNotificationsLocked(@NonNull ObserverInfo observerInfo) {
// Get and clear the pending changes
- Set<DocumentChangeGroupKey> documentChanges = observerInfo.mDocumentChanges;
- if (documentChanges.isEmpty()) {
+ Map<String, Set<String>> schemaChanges = observerInfo.mSchemaChanges;
+ Map<DocumentChangeGroupKey, Set<String>> documentChanges = observerInfo.mDocumentChanges;
+ if (schemaChanges.isEmpty() && documentChanges.isEmpty()) {
return;
}
- observerInfo.mDocumentChanges = new ArraySet<>();
+ if (!schemaChanges.isEmpty()) {
+ observerInfo.mSchemaChanges = new ArrayMap<>();
+ }
+ if (!documentChanges.isEmpty()) {
+ observerInfo.mDocumentChanges = new ArrayMap<>();
+ }
// Dispatch the pending changes
observerInfo.mExecutor.execute(() -> {
- for (DocumentChangeGroupKey entry : documentChanges) {
- // TODO(b/193494000): Buffer document URIs as the values of mDocumentChanges
- // and include them in the final ChangeInfo
- DocumentChangeInfo documentChangeInfo = new DocumentChangeInfo(
- entry.mPackageName,
- entry.mDatabaseName,
- entry.mNamespace,
- entry.mSchemaName);
+ // Schema changes
+ if (!schemaChanges.isEmpty()) {
+ for (Map.Entry<String, Set<String>> entry : schemaChanges.entrySet()) {
+ SchemaChangeInfo schemaChangeInfo = new SchemaChangeInfo(
+ /*packageName=*/PrefixUtil.getPackageName(entry.getKey()),
+ /*databaseName=*/PrefixUtil.getDatabaseName(entry.getKey()),
+ /*changedSchemaNames=*/entry.getValue());
- try {
- // TODO(b/193494000): Add code to dispatch SchemaChangeInfo too.
- observerInfo.mObserver.onDocumentChanged(documentChangeInfo);
- } catch (Throwable t) {
- Log.w(TAG, "AppSearchObserverCallback threw exception during dispatch", t);
+ try {
+ observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
+ } catch (Throwable t) {
+ Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+ }
+ }
+ }
+
+ // Document changes
+ if (!documentChanges.isEmpty()) {
+ for (Map.Entry<DocumentChangeGroupKey, Set<String>> entry
+ : documentChanges.entrySet()) {
+ DocumentChangeInfo documentChangeInfo = new DocumentChangeInfo(
+ entry.getKey().mPackageName,
+ entry.getKey().mDatabaseName,
+ entry.getKey().mNamespace,
+ entry.getKey().mSchemaName,
+ entry.getValue());
+
+ try {
+ observerInfo.mObserverCallback.onDocumentChanged(documentChangeInfo);
+ } catch (Throwable t) {
+ Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+ }
}
}
});
@@ -280,12 +448,6 @@
private static boolean matchesSpec(
@NonNull String schemaType, @NonNull ObserverSpec observerSpec) {
Set<String> schemaFilters = observerSpec.getFilterSchemas();
- if (!schemaFilters.isEmpty() && !schemaFilters.contains(schemaType)) {
- return false;
- }
- // TODO(b/193494000): We also need to check VisibilityStore to see if the observer is
- // allowed to access this type before granting access. Note if fixing this TODO makes the
- // method non-static we need to handle locking.
- return true;
+ return schemaFilters.isEmpty() || schemaFilters.contains(schemaType);
}
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index 761ebfd..8da40bb 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -16,18 +16,15 @@
// @exportToFramework:skipFile()
package androidx.appsearch.localstorage;
-import android.os.Process;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.stats.SearchStats;
import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -40,11 +37,13 @@
private final Executor mExecutor;
- // The package name to search over. If null, this will search over all package names.
- @Nullable
+ /* The package name of the current app which is using the local backend. */
private final String mPackageName;
- // The database name to search over. If null, this will search over all database names.
+ /** A CallerAccess object describing local-only access of the current app. */
+ private final CallerAccess mSelfCallerAccess;
+
+ /* The database name to search over. If null, this will search over all database names. */
@Nullable
private final String mDatabaseName;
@@ -69,14 +68,15 @@
SearchResultsImpl(
@NonNull AppSearchImpl appSearchImpl,
@NonNull Executor executor,
- @Nullable String packageName,
+ @NonNull String packageName,
@Nullable String databaseName,
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec,
@Nullable AppSearchLogger logger) {
mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
mExecutor = Preconditions.checkNotNull(executor);
- mPackageName = packageName;
+ mPackageName = Preconditions.checkNotNull(packageName);
+ mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
mDatabaseName = databaseName;
mQueryExpression = Preconditions.checkNotNull(queryExpression);
mSearchSpec = Preconditions.checkNotNull(searchSpec);
@@ -85,27 +85,17 @@
@Override
@NonNull
- public ListenableFuture<List<SearchResult>> getNextPage() {
+ public ListenableFuture<List<SearchResult>> getNextPageAsync() {
Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
return FutureUtil.execute(mExecutor, () -> {
SearchResultPage searchResultPage;
if (mIsFirstLoad) {
mIsFirstLoad = false;
- if (mPackageName == null) {
- throw new AppSearchException(
- AppSearchResult.RESULT_INVALID_ARGUMENT,
- "Invalid null package name for query");
- } else if (mDatabaseName == null) {
+ if (mDatabaseName == null) {
mVisibilityScope = SearchStats.VISIBILITY_SCOPE_GLOBAL;
// Global queries aren't restricted to a single database
searchResultPage = mAppSearchImpl.globalQuery(
- mQueryExpression,
- mSearchSpec,
- mPackageName,
- /*visibilityStore=*/ null,
- Process.myUid(),
- /*callerHasSystemAccess=*/ false,
- mLogger);
+ mQueryExpression, mSearchSpec, mSelfCallerAccess, mLogger);
} else {
mVisibilityScope = SearchStats.VISIBILITY_SCOPE_LOCAL;
// Normal local query, pass in specified database.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index a695642..8d3ebb1 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -18,6 +18,7 @@
import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
+import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
@@ -31,7 +32,6 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.Migrator;
-import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.ReportUsageRequest;
@@ -40,14 +40,15 @@
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.stats.RemoveStats;
import androidx.appsearch.localstorage.stats.SchemaMigrationStats;
import androidx.appsearch.localstorage.stats.SetSchemaStats;
import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.appsearch.util.SchemaMigrationUtil;
-import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -70,49 +71,50 @@
*/
class SearchSessionImpl implements AppSearchSession {
private static final String TAG = "AppSearchSessionImpl";
+
private final AppSearchImpl mAppSearchImpl;
private final Executor mExecutor;
private final Features mFeatures;
- private final String mPackageName;
+ private final Context mContext;
private final String mDatabaseName;
+ @Nullable private final AppSearchLogger mLogger;
+
+ private final String mPackageName;
+ private final CallerAccess mSelfCallerAccess;
+
private volatile boolean mIsMutated = false;
private volatile boolean mIsClosed = false;
- @Nullable private final AppSearchLogger mLogger;
SearchSessionImpl(
@NonNull AppSearchImpl appSearchImpl,
@NonNull Executor executor,
@NonNull Features features,
- @NonNull String packageName,
+ @NonNull Context context,
@NonNull String databaseName,
@Nullable AppSearchLogger logger) {
mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
mExecutor = Preconditions.checkNotNull(executor);
mFeatures = Preconditions.checkNotNull(features);
- mPackageName = packageName;
+ mContext = Preconditions.checkNotNull(context);
mDatabaseName = Preconditions.checkNotNull(databaseName);
mLogger = logger;
+
+ mPackageName = mContext.getPackageName();
+ mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
}
@Override
@NonNull
- public ListenableFuture<SetSchemaResponse> setSchema(
+ public ListenableFuture<SetSchemaResponse> setSchemaAsync(
@NonNull SetSchemaRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
ListenableFuture<SetSchemaResponse> future = execute(() -> {
long startMillis = SystemClock.elapsedRealtime();
-
- // Convert the inner set into a List since Binder can't handle Set.
- Map<String, Set<PackageIdentifier>> schemasVisibleToPackages =
- request.getSchemasVisibleToPackagesInternal();
- Map<String, List<PackageIdentifier>> copySchemasVisibleToPackages = new ArrayMap<>();
- for (Map.Entry<String, Set<PackageIdentifier>> entry :
- schemasVisibleToPackages.entrySet()) {
- copySchemasVisibleToPackages.put(entry.getKey(),
- new ArrayList<>(entry.getValue()));
- }
+ // Extract a Map<schema, VisibilityDocument> from the request.
+ List<VisibilityDocument> visibilityDocuments = VisibilityDocument
+ .toVisibilityDocuments(request);
SetSchemaStats.Builder setSchemaStatsBuilder = null;
if (mLogger != null) {
@@ -123,20 +125,25 @@
// No need to trigger migration if user never set migrator.
if (migrators.size() == 0) {
SetSchemaResponse setSchemaResponse =
- setSchemaNoMigrations(request, copySchemasVisibleToPackages,
- setSchemaStatsBuilder);
+ setSchemaNoMigrations(request, visibilityDocuments, setSchemaStatsBuilder);
+
+ // Schedule a task to dispatch change notifications. See requirements for where the
+ // method is called documented in the method description.
+ dispatchChangeNotifications();
+
if (setSchemaStatsBuilder != null) {
setSchemaStatsBuilder.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - startMillis));
mLogger.logStats(setSchemaStatsBuilder.build());
}
+
return setSchemaResponse;
}
// Migration process
// 1. Validate and retrieve all active migrators.
- GetSchemaResponse getSchemaResponse =
- mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+ mPackageName, mDatabaseName, mSelfCallerAccess);
int currentVersion = getSchemaResponse.getVersion();
int finalVersion = request.getVersion();
Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
@@ -144,8 +151,7 @@
// No need to trigger migration if no migrator is active.
if (activeMigrators.size() == 0) {
SetSchemaResponse setSchemaResponse =
- setSchemaNoMigrations(request, copySchemasVisibleToPackages,
- setSchemaStatsBuilder);
+ setSchemaNoMigrations(request, visibilityDocuments, setSchemaStatsBuilder);
if (setSchemaStatsBuilder != null) {
setSchemaStatsBuilder.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - startMillis));
@@ -161,9 +167,7 @@
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- /*visibilityStore=*/ null,
- new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
- copySchemasVisibleToPackages,
+ visibilityDocuments,
/*forceOverride=*/false,
request.getVersion(),
setSchemaStatsBuilder);
@@ -197,9 +201,7 @@
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- /*visibilityStore=*/ null,
- new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
- copySchemasVisibleToPackages,
+ visibilityDocuments,
/*forceOverride=*/ true,
request.getVersion(),
setSchemaStatsBuilder);
@@ -214,6 +216,10 @@
migrationHelper.readAndPutDocuments(responseBuilder,
schemaMigrationStatsBuilder);
+ // Schedule a task to dispatch change notifications. See requirements for where the
+ // method is called documented in the method description.
+ dispatchChangeNotifications();
+
if (schemaMigrationStatsBuilder != null) {
long endMillis = SystemClock.elapsedRealtime();
schemaMigrationStatsBuilder
@@ -251,14 +257,15 @@
@Override
@NonNull
- public ListenableFuture<GetSchemaResponse> getSchema() {
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
- return execute(() -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName));
+ return execute(
+ () -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName, mSelfCallerAccess));
}
@NonNull
@Override
- public ListenableFuture<Set<String>> getNamespaces() {
+ public ListenableFuture<Set<String>> getNamespacesAsync() {
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
List<String> namespaces = mAppSearchImpl.getNamespaces(mPackageName, mDatabaseName);
@@ -268,7 +275,7 @@
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+ public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
@NonNull PutDocumentsRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -303,7 +310,7 @@
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
@NonNull GetByDocumentIdRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -346,7 +353,7 @@
@Override
@NonNull
- public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+ public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
@@ -364,7 +371,7 @@
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+ public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
@NonNull RemoveByDocumentIdRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -403,7 +410,7 @@
@Override
@NonNull
- public ListenableFuture<Void> remove(
+ public ListenableFuture<Void> removeAsync(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpec);
@@ -432,14 +439,14 @@
@Override
@NonNull
- public ListenableFuture<StorageInfo> getStorageInfo() {
+ public ListenableFuture<StorageInfo> getStorageInfoAsync() {
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> mAppSearchImpl.getStorageInfoForDatabase(mPackageName, mDatabaseName));
}
@NonNull
@Override
- public ListenableFuture<Void> requestFlush() {
+ public ListenableFuture<Void> requestFlushAsync() {
return execute(() -> {
mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
return null;
@@ -472,20 +479,18 @@
/**
* Set schema to Icing for no-migration scenario.
*
- * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
+ * <p>We only need one time {@link #setSchemaAsync} call for no-migration scenario by using the
* forceoverride in the request.
*/
private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
- @NonNull Map<String, List<PackageIdentifier>> copySchemasVisibleToPackages,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
SetSchemaStats.Builder setSchemaStatsBuilder)
throws AppSearchException {
SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- /*visibilityStore=*/ null,
- new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
- copySchemasVisibleToPackages,
+ visibilityDocuments,
request.isForceOverride(),
request.getVersion(),
setSchemaStatsBuilder);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index af24c98..e3c0a7da 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -17,7 +17,6 @@
package androidx.appsearch.localstorage.converter;
import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
-import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
@@ -28,7 +27,10 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -152,36 +154,30 @@
* For each target schema, we will check visibility store is that accessible to the caller. And
* remove this schemas if it is not allowed for caller to query.
*
- * @param callerPackageName The package name of caller
- * @param visibilityStore The visibility store which holds all visibility settings.
- * If you pass null, all schemas that don't belong to the
- * caller package will be removed.
- * @param callerUid The uid of the caller.
- * @param callerHasSystemAccess Whether the caller has system access.
+ * @param callerAccess Visibility access info of the calling app
+ * @param visibilityStore The {@link VisibilityStore} that store all visibility
+ * information.
+ * @param visibilityChecker Optional visibility checker to check whether the caller
+ * could access target schemas. Pass {@code null} will
+ * reject access for all documents which doesn't belong
+ * to the calling package.
*/
- public void removeInaccessibleSchemaFilter(@NonNull String callerPackageName,
+ public void removeInaccessibleSchemaFilter(
+ @NonNull CallerAccess callerAccess,
@Nullable VisibilityStore visibilityStore,
- int callerUid,
- boolean callerHasSystemAccess) {
+ @Nullable VisibilityChecker visibilityChecker) {
Iterator<String> targetPrefixedSchemaFilterIterator =
mTargetPrefixedSchemaFilters.iterator();
while (targetPrefixedSchemaFilterIterator.hasNext()) {
String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
String packageName = getPackageName(targetPrefixedSchemaFilter);
- boolean allow;
- if (packageName.equals(callerPackageName)) {
- // Callers can always retrieve their own data
- allow = true;
- } else if (visibilityStore == null) {
- // If there's no visibility store, there's no extra access
- allow = false;
- } else {
- String databaseName = getDatabaseName(targetPrefixedSchemaFilter);
- allow = visibilityStore.isSchemaSearchableByCaller(packageName, databaseName,
- targetPrefixedSchemaFilter, callerUid, callerHasSystemAccess);
- }
- if (!allow) {
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ callerAccess,
+ packageName,
+ targetPrefixedSchemaFilter,
+ visibilityStore,
+ visibilityChecker)) {
targetPrefixedSchemaFilterIterator.remove();
}
}
@@ -229,7 +225,7 @@
ResultSpecProto.SnippetSpecProto.newBuilder()
.setNumToSnippet(mSearchSpec.getSnippetCount())
.setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
- .setMaxWindowBytes(mSearchSpec.getMaxSnippetSize()));
+ .setMaxWindowUtf32Length(mSearchSpec.getMaxSnippetSize()));
// Rewrites the typePropertyMasks that exist in {@code prefixes}.
int groupingType = mSearchSpec.getResultGroupingTypeFlags();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
index e3abf90..101ef1f 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
@@ -56,6 +56,7 @@
CALL_TYPE_GLOBAL_SEARCH,
CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH,
+ CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallType {
@@ -76,6 +77,7 @@
public static final int CALL_TYPE_GLOBAL_SEARCH = 12;
public static final int CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH = 13;
public static final int CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH = 14;
+ public static final int CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID = 15;
@Nullable
private final String mPackageName;
@@ -145,11 +147,11 @@
* Returns number of operations succeeded.
*
* <p>For example, for
- * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of individual
* successful put operations. In this case, how many documents are successfully indexed.
*
* <p>For non-batch calls such as
- * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
* {@link CallStats#getNumOperationsSucceeded()} and
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
@@ -162,11 +164,12 @@
* Returns number of operations failed.
*
* <p>For example, for
- * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of individual
* failed put operations. In this case, how many documents are failed to be indexed.
*
- * <p>For non-batch calls such as {@link androidx.appsearch.app.AppSearchSession#setSchema},
- * the sum of {@link CallStats#getNumOperationsSucceeded()} and
+ * <p>For non-batch calls such as
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
+ * {@link CallStats#getNumOperationsSucceeded()} and
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
*/
@@ -235,12 +238,12 @@
* Sets number of operations succeeded.
*
* <p>For example, for
- * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of
* individual successful put operations. In this case, how many documents are
* successfully indexed.
*
* <p>For non-batch calls such as
- * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
* {@link CallStats#getNumOperationsSucceeded()} and
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
@@ -254,12 +257,12 @@
/**
* Sets number of operations failed.
*
- * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#put}, it is the
+ * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the
* total number of individual failed put operations. In this case, how many documents
* are failed to be indexed.
*
* <p>For non-batch calls such as
- * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
* {@link CallStats#getNumOperationsSucceeded()} and
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
index c8bc687..e9a25fd 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -23,7 +23,7 @@
/**
* A class for holding detailed stats to log for each individual document put by a
- * {@link androidx.appsearch.app.AppSearchSession#put} call.
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync} call.
*
* @hide
*/
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
index 3a63aa0..7eb4820 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
@@ -29,8 +29,8 @@
/**
* Class holds detailed stats for
- * {@link androidx.appsearch.app.AppSearchSession#remove(RemoveByDocumentIdRequest)} and
- * {@link androidx.appsearch.app.AppSearchSession#remove(String, SearchSpec)}
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync(RemoveByDocumentIdRequest)} and
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync(String, SearchSpec)}
*
* @hide
*/
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
index 12d4587..cf0bde3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
@@ -25,10 +25,13 @@
import androidx.appsearch.exceptions.AppSearchException;
import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
/**
* Provides utility functions for working with package + database prefixes.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -41,7 +44,8 @@
@VisibleForTesting
public static final char PACKAGE_DELIMITER = '$';
- private PrefixUtil() {}
+ private PrefixUtil() {
+ }
/**
* Creates prefix string for given package name and database name.
@@ -50,6 +54,7 @@
public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
}
+
/**
* Creates prefix string for given package name.
*/
@@ -231,4 +236,37 @@
return schemaPrefix;
}
+
+ /**
+ * Removes any prefixes from types mentioned anywhere in {@code typeConfigBuilder}.
+ *
+ * @param typeConfigBuilder The schema type to mutate
+ * @return Prefix name that was removed from the schema type.
+ * @throws AppSearchException if there are unexpected database prefixing errors.
+ */
+ @NonNull
+ public static String removePrefixesFromSchemaType(
+ @NonNull SchemaTypeConfigProto.Builder typeConfigBuilder)
+ throws AppSearchException {
+ String typePrefix = PrefixUtil.getPrefix(typeConfigBuilder.getSchemaType());
+ // Rewrite SchemaProto.types.schema_type
+ String newSchemaType =
+ typeConfigBuilder.getSchemaType().substring(typePrefix.length());
+ typeConfigBuilder.setSchemaType(newSchemaType);
+
+ // Rewrite SchemaProto.types.properties.schema_type
+ for (int propertyIdx = 0;
+ propertyIdx < typeConfigBuilder.getPropertiesCount();
+ propertyIdx++) {
+ if (!typeConfigBuilder.getProperties(propertyIdx).getSchemaType().isEmpty()) {
+ PropertyConfigProto.Builder propertyConfigBuilder =
+ typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+ String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+ .substring(typePrefix.length());
+ propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+ typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+ }
+ }
+ return typePrefix;
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
new file mode 100644
index 0000000..1a47109
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Contains attributes of an API caller relevant to its access via visibility store.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CallerAccess {
+ private final String mCallingPackageName;
+
+ /**
+ * Constructs a new {@link CallerAccess}.
+ *
+ * @param callingPackageName The name of the package which wants to access data.
+ */
+ public CallerAccess(@NonNull String callingPackageName) {
+ mCallingPackageName = Preconditions.checkNotNull(callingPackageName);
+ }
+
+ /** Returns the name of the package which wants to access data. */
+ @NonNull
+ public String getCallingPackageName() {
+ return mCallingPackageName;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CallerAccess)) return false;
+ CallerAccess that = (CallerAccess) o;
+ return mCallingPackageName.equals(that.mCallingPackageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return mCallingPackageName.hashCode();
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
new file mode 100644
index 0000000..7801562
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * An interface for classes that validate document visibility data.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface VisibilityChecker {
+ /**
+ * Checks whether the given caller has access to the given prefixed schemas.
+ *
+ * @param callerAccess Visibility access info of the calling app
+ * @param packageName Package of app that owns the schemas.
+ * @param prefixedSchema The prefixed schema type that the caller want to access.
+ * @param visibilityStore The {@link VisibilityStore} that store all visibility information.
+ */
+ boolean isSchemaSearchableByCaller(
+ @NonNull CallerAccess callerAccess,
+ @NonNull String packageName,
+ @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore);
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
new file mode 100644
index 0000000..537fd81
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.Set;
+
+/**
+ * Holds the visibility settings in version 1 that apply to a schema type.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class VisibilityDocumentV1 extends GenericDocument {
+ /**
+ * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+ */
+ static final String SCHEMA_TYPE = "VisibilityType";
+ /** Namespace of documents that contain visibility settings */
+ static final String NAMESPACE = "";
+
+ /**
+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+ */
+ private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+ /** Property that holds the package name that can access a schema. */
+ private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+ /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+ private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+ /** Property that holds the role can access a schema. */
+ private static final String ROLE_PROPERTY = "role";
+
+ /** Property that holds the required permissions to access the schema. */
+ private static final String PERMISSION_PROPERTY = "permission";
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ */
+ static final AppSearchSchema
+ SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.LongPropertyConfig.Builder(ROLE_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.LongPropertyConfig.Builder(PERMISSION_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ VisibilityDocumentV1(@NonNull GenericDocument genericDocument) {
+ super(genericDocument);
+ }
+
+ /** Returns whether this schema is visible to the system. */
+ boolean isNotDisplayedBySystem() {
+ return getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+ }
+
+ /**
+ * Returns a package name array which could access this schema. Use {@link #getSha256Certs()}
+ * to get package's sha 256 certs. The same index of package names array and sha256Certs array
+ * represents same package.
+ */
+ @NonNull
+ String[] getPackageNames() {
+ return getPropertyStringArray(PACKAGE_NAME_PROPERTY);
+ }
+
+ /**
+ * Returns a package sha256Certs array which could access this schema. Use
+ * {@link #getPackageNames()} to get package's name. The same index of package names array
+ * and sha256Certs array represents same package.
+ */
+ @NonNull
+ byte[][] getSha256Certs() {
+ return getPropertyBytesArray(SHA_256_CERT_PROPERTY);
+ }
+
+ /**
+ * Returns an array of Android Roles that have access to the schema this
+ * {@link VisibilityDocumentV1} represents.
+ */
+ @Nullable
+ Set<Integer> getVisibleToRoles() {
+ return toInts(getPropertyLongArray(ROLE_PROPERTY));
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema
+ * this {@link VisibilityDocumentV1} represents.
+ */
+ @Nullable
+ Set<Integer> getVisibleToPermissions() {
+ return toInts(getPropertyLongArray(PERMISSION_PROPERTY));
+ }
+
+ /** Builder for {@link VisibilityDocumentV1}. */
+ static class Builder extends GenericDocument.Builder<Builder> {
+ private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
+
+ /**
+ * Creates a {@link Builder} for a {@link VisibilityDocumentV1}.
+ *
+ * @param id The SchemaType of the {@link AppSearchSchema} that this
+ * {@link VisibilityDocumentV1} represents. The package and database prefix will
+ * be added in server side. We are using prefixed schema type to be the final
+ * id of this {@link VisibilityDocumentV1}.
+ */
+ Builder(@NonNull String id) {
+ super(NAMESPACE, id, SCHEMA_TYPE);
+ }
+
+ /** Sets whether this schema has opted out of platform surfacing. */
+ @NonNull
+ Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+ return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+ notDisplayedBySystem);
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @NonNull
+ Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
+ Preconditions.checkNotNull(packageIdentifiers);
+ mPackageIdentifiers.addAll(packageIdentifiers);
+ return this;
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @NonNull
+ Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+ Preconditions.checkNotNull(packageIdentifier);
+ mPackageIdentifiers.add(packageIdentifier);
+ return this;
+ }
+
+ /** Add a set of Android role that has access to the schema this
+ * {@link VisibilityDocumentV1} represents. */
+ @NonNull
+ Builder setVisibleToRoles(@NonNull Set<Integer> visibleToRoles) {
+ Preconditions.checkNotNull(visibleToRoles);
+ setPropertyLong(ROLE_PROPERTY, toLongs(visibleToRoles));
+ return this;
+ }
+
+ /** Add a set of Android role that has access to the schema this
+ * {@link VisibilityDocumentV1} represents. */
+ @NonNull
+ Builder setVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
+ Preconditions.checkNotNull(visibleToPermissions);
+ setPropertyLong(PERMISSION_PROPERTY, toLongs(visibleToPermissions));
+ return this;
+ }
+
+ /** Build a {@link VisibilityDocumentV1} */
+ @Override
+ @NonNull
+ public VisibilityDocumentV1 build() {
+ String[] packageNames = new String[mPackageIdentifiers.size()];
+ byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];
+ int i = 0;
+ for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {
+ packageNames[i] = packageIdentifier.getPackageName();
+ sha256Certs[i] = packageIdentifier.getSha256Certificate();
+ ++i;
+ }
+ setPropertyString(PACKAGE_NAME_PROPERTY, packageNames);
+ setPropertyBytes(SHA_256_CERT_PROPERTY, sha256Certs);
+ return new VisibilityDocumentV1(super.build());
+ }
+ }
+
+ @NonNull
+ static long[] toLongs(@NonNull Set<Integer> properties) {
+ long[] outputs = new long[properties.size()];
+ int i = 0;
+ for (int property : properties) {
+ outputs[i++] = property;
+ }
+ return outputs;
+ }
+
+ @Nullable
+ private static Set<Integer> toInts(@Nullable long[] properties) {
+ if (properties == null) {
+ return null;
+ }
+ Set<Integer> outputs = new ArraySet<>(properties.length);
+ for (long property : properties) {
+ outputs.add((int) property);
+ }
+ return outputs;
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
index d3d754e..79e22ff5c 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
@@ -15,60 +15,246 @@
*/
package androidx.appsearch.localstorage.visibilitystore;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.exceptions.AppSearchException;
+import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.app.VisibilityPermissionDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.PersistType;
+
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
- * An interface for classes that store and validate document visibility data.
+ * Stores all visibility settings for all databases that AppSearchImpl knows about.
+ * Persists the visibility settings and reloads them on initialization.
+ *
+ * <p>The VisibilityStore creates a {@link VisibilityDocument} for each schema. This document holds
+ * the visibility settings that apply to that schema. The VisibilityStore also creates a
+ * schema for these documents and has its own package and database so that its data doesn't
+ * interfere with any clients' data. It persists the document and schema through AppSearchImpl.
+ *
+ * <p>These visibility settings won't be used in AppSearch Jetpack, we only store them for clients
+ * to look up.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface VisibilityStore {
+public class VisibilityStore {
+ private static final String TAG = "AppSearchVisibilityStor";
/**
* These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
* AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
*/
- String PACKAGE_NAME = "VS#Pkg";
+ public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg";
- @VisibleForTesting
- String DATABASE_NAME = "VS#Db";
+ static final String VISIBILITY_DATABASE_NAME = "VS#Db";
/**
- * Sets visibility settings for the given database. Any previous visibility settings will be
- * overwritten.
+ * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each
+ * schema type.
+ */
+ private final Map<String, VisibilityDocument> mVisibilityDocumentMap = new ArrayMap<>();
+
+ private final AppSearchImpl mAppSearchImpl;
+
+ public VisibilityStore(@NonNull AppSearchImpl appSearchImpl)
+ throws AppSearchException {
+ mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
+
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
+ List<VisibilityDocumentV1> visibilityDocumentsV1s = null;
+ switch (getSchemaResponse.getVersion()) {
+ case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:
+ // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using
+ // GenericDocument.
+ List<GenericDocument> visibilityDocumentsV0s =
+ VisibilityStoreMigrationHelperFromV0.getVisibilityDocumentsInVersion0(
+ getSchemaResponse, mAppSearchImpl);
+ visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0
+ .toVisibilityDocumentV1(visibilityDocumentsV0s);
+ // fall through
+ case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA:
+ if (visibilityDocumentsV1s == null) {
+ // We need to read VisibilityDocument in Version 1 from AppSearch instead of
+ // taking from the above step.
+ visibilityDocumentsV1s =
+ VisibilityStoreMigrationHelperFromV1.getVisibilityDocumentsInVersion1(
+ mAppSearchImpl);
+ }
+ setLatestSchemaAndDocuments(VisibilityStoreMigrationHelperFromV1
+ .toVisibilityDocumentsV2(visibilityDocumentsV1s));
+ break;
+ case VisibilityDocument.SCHEMA_VERSION_LATEST:
+ Set<AppSearchSchema> existingVisibilitySchema = getSchemaResponse.getSchemas();
+ if (existingVisibilitySchema.contains(VisibilityDocument.SCHEMA)
+ && existingVisibilitySchema.contains(VisibilityPermissionDocument.SCHEMA)) {
+ // The latest Visibility schema is in AppSearch, we must find our schema type.
+ // Extract all stored Visibility Document into mVisibilityDocumentMap.
+ loadVisibilityDocumentMap();
+ } else {
+ // We must have a broken schema. Reset it to the latest version.
+ // Do NOT set forceOverride to be true here. If you hit problem here it means
+ // you made a incompatible change in Visibility Schema without update the
+ // version number. You should bump the version number and create a
+ // VisibilityStoreMigrationHelper which can analyse the different between the
+ // old version and the new version to migration user's visibility settings.
+ mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ Arrays.asList(VisibilityDocument.SCHEMA,
+ VisibilityPermissionDocument.SCHEMA),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ }
+ break;
+ default:
+ // We must did something wrong.
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ "Found unsupported visibility version: " + getSchemaResponse.getVersion());
+ }
+ }
+
+ /**
+ * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous
+ * {@link VisibilityDocument}s with same prefixed schema type will be overwritten.
*
- * @param packageName Package of app that owns the schemas.
- * @param databaseName Database that owns the schemas.
- * @param schemasNotDisplayedBySystem Set of prefixed schemas that should be hidden from
- * platform surfaces.
- * @param schemasVisibleToPackages Map of prefixed schemas to a list of package identifiers that
- * have access to the schema.
+ * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which
+ * contains schema type's visibility information.
* @throws AppSearchException on AppSearchImpl error.
*/
- void setVisibility(
- @NonNull String packageName,
- @NonNull String databaseName,
- @NonNull Set<String> schemasNotDisplayedBySystem,
- @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
- throws AppSearchException;
+ public void setVisibility(@NonNull List<VisibilityDocument> prefixedVisibilityDocuments)
+ throws AppSearchException {
+ Preconditions.checkNotNull(prefixedVisibilityDocuments);
+ // Save new setting.
+ for (int i = 0; i < prefixedVisibilityDocuments.size(); i++) {
+ // put VisibilityDocument to AppSearchImpl and mVisibilityDocumentMap. If there is a
+ // VisibilityDocument with same prefixed schema exists, it will be replaced by new
+ // VisibilityDocument in both AppSearch and memory look up map.
+ VisibilityDocument prefixedVisibilityDocument = prefixedVisibilityDocuments.get(i);
+ mAppSearchImpl.putDocument(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ prefixedVisibilityDocument, /*logger=*/ null);
+ mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),
+ prefixedVisibilityDocument);
+ }
+ // Now that the visibility document has been written. Persist the newly written data.
+ mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+ }
/**
- * Checks whether the given package has access to system-surfaceable schemas.
- *
- * @param callerUid UID of the app that wants to see the data.
+ * Remove the visibility setting for the given prefixed schema type from both AppSearch and
+ * memory look up map.
*/
- boolean isSchemaSearchableByCaller(
- @NonNull String packageName,
- @NonNull String databaseName,
- @NonNull String prefixedSchema,
- int callerUid,
- boolean callerHasSystemAccess);
+ public void removeVisibility(@NonNull Set<String> prefixedSchemaTypes)
+ throws AppSearchException {
+ for (String prefixedSchemaType : prefixedSchemaTypes) {
+ if (mVisibilityDocumentMap.remove(prefixedSchemaType) == null) {
+ // The deleted schema is not all-default setting, we need to remove its
+ // VisibilityDocument from Icing.
+ try {
+ mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE, prefixedSchemaType,
+ /*removeStatsBuilder=*/null);
+ } catch (AppSearchException e) {
+ if (e.getResultCode() == RESULT_NOT_FOUND) {
+ // We are trying to remove this visibility setting, so it's weird but seems
+ // to be fine if we cannot find it.
+ Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType
+ + " to remove.");
+ return;
+ }
+ throw e;
+ }
+ }
+ }
+ }
+
+ /** Gets the {@link VisibilityDocument} for the given prefixed schema type. */
+ @Nullable
+ public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) {
+ return mVisibilityDocumentMap.get(prefixedSchemaType);
+ }
+
+ /**
+ * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into
+ * {@link #mVisibilityDocumentMap}.
+ */
+ private void loadVisibilityDocumentMap() throws AppSearchException {
+ // Populate visibility settings set
+ List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes();
+ for (int i = 0; i < cachedSchemaTypes.size(); i++) {
+ String prefixedSchemaType = cachedSchemaTypes.get(i);
+ String packageName = PrefixUtil.getPackageName(prefixedSchemaType);
+ if (packageName.equals(VISIBILITY_PACKAGE_NAME)) {
+ continue; // Our own package. Skip.
+ }
+
+ VisibilityDocument visibilityDocument;
+ try {
+ // Note: We use the other clients' prefixed schema type as ids
+ visibilityDocument = new VisibilityDocument(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ /*id=*/ prefixedSchemaType,
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ } catch (AppSearchException e) {
+ if (e.getResultCode() == RESULT_NOT_FOUND) {
+ // The schema has all default setting and we won't have a VisibilityDocument for
+ // it.
+ continue;
+ }
+ // Otherwise, this is some other error we should pass up.
+ throw e;
+ }
+ mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);
+ }
+ }
+
+ /**
+ * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch.
+ */
+ private void setLatestSchemaAndDocuments(@NonNull List<VisibilityDocument> migratedDocuments)
+ throws AppSearchException {
+ // The latest schema type doesn't exist yet. Add it. Set forceOverride true to
+ // delete old schema.
+ mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ Arrays.asList(VisibilityDocument.SCHEMA,
+ VisibilityPermissionDocument.SCHEMA),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ for (int i = 0; i < migratedDocuments.size(); i++) {
+ VisibilityDocument migratedDocument = migratedDocuments.get(i);
+ mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument);
+ mAppSearchImpl.putDocument(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ migratedDocument, /*logger=*/ null);
+ }
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
new file mode 100644
index 0000000..edff437
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The helper class to store Visibility Document information of version 0 and handle the upgrade to
+ * version 1.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityStoreMigrationHelperFromV0 {
+ private VisibilityStoreMigrationHelperFromV0() {}
+ /** Prefix to add to all visibility document ids. IcingSearchEngine doesn't allow empty ids. */
+ private static final String DEPRECATED_ID_PREFIX = "uri:";
+
+ /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
+ @VisibleForTesting
+ static final String DEPRECATED_VISIBILITY_SCHEMA_TYPE = "VisibilityType";
+
+ /**
+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+ */
+ @VisibleForTesting
+ static final String DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY =
+ "notPlatformSurfaceable";
+
+ /** Property that holds nested documents of package accessible schemas. */
+ @VisibleForTesting
+ static final String DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY = "packageAccessible";
+
+ /**
+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+ */
+ @VisibleForTesting
+ static final String DEPRECATED_PACKAGE_SCHEMA_TYPE = "PackageAccessibleType";
+
+ /** Property that holds the prefixed schema type that is accessible by some package. */
+ @VisibleForTesting
+ static final String DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
+
+ /** Property that holds the package name that can access a schema. */
+ @VisibleForTesting
+ static final String DEPRECATED_PACKAGE_NAME_PROPERTY = "packageName";
+
+ /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+ @VisibleForTesting
+ static final String DEPRECATED_SHA_256_CERT_PROPERTY = "sha256Cert";
+
+// The visibility schema of version 0.
+//---------------------------------------------------------------------------------------------
+// Schema of DEPRECATED_VISIBILITY_SCHEMA_TYPE:
+// new AppSearchSchema.Builder(
+// DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+// .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+// DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+// .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+// .build())
+// .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+// DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+// DEPRECATED_PACKAGE_SCHEMA_TYPE)
+// .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+// .build())
+// .build();
+// Schema of DEPRECATED_PACKAGE_SCHEMA_TYPE:
+// new AppSearchSchema.Builder(DEPRECATED_PACKAGE_SCHEMA_TYPE)
+// .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+// DEPRECATED_PACKAGE_NAME_PROPERTY)
+// .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+// .build())
+// .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+// DEPRECATED_SHA_256_CERT_PROPERTY)
+// .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+// .build())
+// .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+// DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+// .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+// .build())
+// .build();
+//---------------------------------------------------------------------------------------------
+
+ /** Returns whether the given schema type is deprecated. */
+ static boolean isDeprecatedType(@NonNull String schemaType) {
+ return schemaType.equals(DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+ || schemaType.equals(DEPRECATED_PACKAGE_SCHEMA_TYPE);
+ }
+
+ /**
+ * Adds a prefix to create a deprecated visibility document's id.
+ *
+ * @param packageName Package to which the visibility doc refers.
+ * @param databaseName Database to which the visibility doc refers.
+ * @return deprecated visibility document's id.
+ */
+ @NonNull
+ static String getDeprecatedVisibilityDocumentId(
+ @NonNull String packageName, @NonNull String databaseName) {
+ return DEPRECATED_ID_PREFIX + PrefixUtil.createPrefix(packageName, databaseName);
+ }
+
+ /** Reads all stored deprecated Visibility Document in version 0 from icing. */
+ static List<GenericDocument> getVisibilityDocumentsInVersion0(
+ @NonNull GetSchemaResponse getSchemaResponse,
+ @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
+ if (!hasDeprecatedType(getSchemaResponse)) {
+ return new ArrayList<>();
+ }
+ Map<String, Set<String>> packageToDatabases = appSearchImpl.getPackageToDatabases();
+ List<GenericDocument> deprecatedDocuments = new ArrayList<>(packageToDatabases.size());
+ for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
+ String packageName = entry.getKey();
+ if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
+ continue; // Our own package. Skip.
+ }
+ for (String databaseName : entry.getValue()) {
+ try {
+ // Note: We use the other clients' prefixed names as ids
+ deprecatedDocuments.add(appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ getDeprecatedVisibilityDocumentId(packageName, databaseName),
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ } catch (AppSearchException e) {
+ if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
+ // TODO(b/172068212): This indicates some desync error. We were expecting a
+ // document, but didn't find one. Should probably reset AppSearch instead
+ // of ignoring it.
+ continue;
+ }
+ // Otherwise, this is some other error we should pass up.
+ throw e;
+ }
+ }
+ }
+ return deprecatedDocuments;
+ }
+
+ /**
+ * Converts the given list of deprecated Visibility Documents into a Map of {@code
+ * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
+ *
+ * @param visibilityDocumentV0s The deprecated Visibility Document we found.
+ */
+ @NonNull
+ static List<VisibilityDocumentV1> toVisibilityDocumentV1(
+ @NonNull List<GenericDocument> visibilityDocumentV0s) {
+ Map<String, VisibilityDocumentV1.Builder> documentBuilderMap = new ArrayMap<>();
+
+ // Set all visibility information into documentBuilderMap
+ for (int i = 0; i < visibilityDocumentV0s.size(); i++) {
+ GenericDocument visibilityDocumentV0 = visibilityDocumentV0s.get(i);
+
+ // Read not displayed by system property field.
+ String[] notDisplayedBySystemSchemas = visibilityDocumentV0.getPropertyStringArray(
+ DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+ if (notDisplayedBySystemSchemas != null) {
+ for (String notDisplayedBySystemSchema : notDisplayedBySystemSchemas) {
+ // SetSchemaRequest.Builder.build() make sure all schemas that has visibility
+ // setting must present in the requests.
+ VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
+ documentBuilderMap, notDisplayedBySystemSchema);
+ visibilityBuilder.setNotDisplayedBySystem(true);
+ }
+ }
+
+ // Read visible to packages field.
+ GenericDocument[] deprecatedPackageDocuments = visibilityDocumentV0
+ .getPropertyDocumentArray(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY);
+ if (deprecatedPackageDocuments != null) {
+ for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
+ String prefixedSchemaType = deprecatedPackageDocument
+ .getPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY);
+ VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
+ documentBuilderMap, prefixedSchemaType);
+ visibilityBuilder.addVisibleToPackage(new PackageIdentifier(
+ deprecatedPackageDocument.getPropertyString(
+ DEPRECATED_PACKAGE_NAME_PROPERTY),
+ deprecatedPackageDocument.getPropertyBytes(
+ DEPRECATED_SHA_256_CERT_PROPERTY)));
+ }
+ }
+ }
+ List<VisibilityDocumentV1> visibilityDocumentsV1 =
+ new ArrayList<>(documentBuilderMap.size());
+ for (Map.Entry<String, VisibilityDocumentV1.Builder> entry :
+ documentBuilderMap.entrySet()) {
+ visibilityDocumentsV1.add(entry.getValue().build());
+ }
+ return visibilityDocumentsV1;
+ }
+
+ /**
+ * Return whether the database maybe has the oldest version of deprecated schema.
+ *
+ * <p> Since the current version number is 0, it is possible that the database is just empty
+ * and it return 0 as the default version number. So we need to check if the deprecated document
+ * presents to trigger the migration.
+ */
+ private static boolean hasDeprecatedType(@NonNull GetSchemaResponse getSchemaResponse) {
+ for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
+ if (VisibilityStoreMigrationHelperFromV0
+ .isDeprecatedType(schema.getSchemaType())) {
+ // Found deprecated type, we need to migrate visibility Document. And it's
+ // not possible for us to find the latest visibility schema.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @NonNull
+ private static VisibilityDocumentV1.Builder getOrCreateBuilder(
+ @NonNull Map<String, VisibilityDocumentV1.Builder> documentBuilderMap,
+ @NonNull String schemaType) {
+ VisibilityDocumentV1.Builder builder = documentBuilderMap.get(schemaType);
+ if (builder == null) {
+ builder = new VisibilityDocumentV1.Builder(/*id=*/ schemaType);
+ documentBuilderMap.put(schemaType, builder);
+ }
+ return builder;
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
new file mode 100644
index 0000000..cd3e2c5
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The helper class to store Visibility Document information of version 1 and handle the upgrade to
+ * latest version
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityStoreMigrationHelperFromV1 {
+ private VisibilityStoreMigrationHelperFromV1() {}
+
+ /** Enum in {@link androidx.appsearch.app.SetSchemaRequest} AppSearch supported role. */
+ @VisibleForTesting
+ static final int DEPRECATED_ROLE_HOME = 1;
+
+ /** Enum in {@link androidx.appsearch.app.SetSchemaRequest} AppSearch supported role. */
+ @VisibleForTesting
+ static final int DEPRECATED_ROLE_ASSISTANT = 2;
+
+ /** Reads all stored deprecated Visibility Document in version 0 from icing. */
+ static List<VisibilityDocumentV1> getVisibilityDocumentsInVersion1(
+ @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
+ List<String> allPrefixedSchemaTypes = appSearchImpl.getAllPrefixedSchemaTypes();
+ List<VisibilityDocumentV1> visibilityDocumentV1s =
+ new ArrayList<>(allPrefixedSchemaTypes.size());
+ for (int i = 0; i < allPrefixedSchemaTypes.size(); i++) {
+ String packageName = PrefixUtil.getPackageName(allPrefixedSchemaTypes.get(i));
+ if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
+ continue; // Our own package. Skip.
+ }
+ try {
+ // Note: We use the prefixed schema type as ids
+ visibilityDocumentV1s.add(new VisibilityDocumentV1(appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityDocument.NAMESPACE,
+ allPrefixedSchemaTypes.get(i),
+ /*typePropertyPaths=*/ Collections.emptyMap())));
+ } catch (AppSearchException e) {
+ if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
+ // TODO(b/172068212): This indicates some desync error. We were expecting a
+ // document, but didn't find one. Should probably reset AppSearch instead
+ // of ignoring it.
+ continue;
+ }
+ // Otherwise, this is some other error we should pass up.
+ throw e;
+ }
+ }
+ return visibilityDocumentV1s;
+ }
+
+ /**
+ * Converts the given list of deprecated Visibility Documents into a Map of {@code
+ * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
+ *
+ * @param visibilityDocumentV1s The deprecated Visibility Document we found.
+ */
+ @NonNull
+ static List<VisibilityDocument> toVisibilityDocumentsV2(
+ @NonNull List<VisibilityDocumentV1> visibilityDocumentV1s) {
+ List<VisibilityDocument> latestVisibilityDocuments =
+ new ArrayList<>(visibilityDocumentV1s.size());
+ for (int i = 0; i < visibilityDocumentV1s.size(); i++) {
+ VisibilityDocumentV1 visibilityDocumentV1 = visibilityDocumentV1s.get(i);
+ Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
+ Set<Integer> deprecatedVisibleToRoles = visibilityDocumentV1.getVisibleToRoles();
+ if (deprecatedVisibleToRoles != null) {
+ for (int deprecatedVisibleToRole : deprecatedVisibleToRoles) {
+ Set<Integer> visibleToPermission = new ArraySet<>();
+ switch (deprecatedVisibleToRole) {
+ case DEPRECATED_ROLE_HOME:
+ visibleToPermission.add(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA);
+ break;
+ case DEPRECATED_ROLE_ASSISTANT:
+ visibleToPermission.add(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA);
+ break;
+ }
+ visibleToPermissions.add(visibleToPermission);
+ }
+ }
+ Set<Integer> deprecatedVisibleToPermissions =
+ visibilityDocumentV1.getVisibleToPermissions();
+ if (deprecatedVisibleToPermissions != null) {
+ visibleToPermissions.add(deprecatedVisibleToPermissions);
+ }
+
+ Set<PackageIdentifier> packageIdentifiers = new ArraySet<>();
+ String[] packageNames = visibilityDocumentV1.getPackageNames();
+ byte[][] sha256Certs = visibilityDocumentV1.getSha256Certs();
+ if (packageNames.length == sha256Certs.length) {
+ for (int j = 0; j < packageNames.length; j++) {
+ packageIdentifiers.add(new PackageIdentifier(packageNames[j], sha256Certs[j]));
+ }
+ }
+ VisibilityDocument.Builder latestVisibilityDocumentBuilder =
+ new VisibilityDocument.Builder(visibilityDocumentV1.getId())
+ .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem())
+ .addVisibleToPackages(packageIdentifiers);
+ if (!visibleToPermissions.isEmpty()) {
+ latestVisibilityDocumentBuilder.setVisibleToPermissions(visibleToPermissions);
+ }
+ latestVisibilityDocuments.add(latestVisibilityDocumentBuilder.build());
+ }
+ return latestVisibilityDocuments;
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
new file mode 100644
index 0000000..ea04968
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityUtil {
+ private VisibilityUtil() {}
+
+ /**
+ * Determines whether the calling package has access to the given prefixed schema type.
+ *
+ * <p>Correctly handles access to own data and the situation that visibilityStore and
+ * visibilityChecker are not configured.
+ *
+ * @param callerAccess Visibility access info of the calling app
+ * @param targetPackageName The package name of the app that owns the data.
+ * @param prefixedSchema The prefixed schema type the caller wants to access.
+ * @param visibilityStore Store for visibility information. If not provided, only
+ * access to own data will be allowed.
+ * @param visibilityChecker Checker for visibility access. If not provided, only access to
+ * own data will be allowed.
+ * @return Whether access by the caller to this prefixed schema should be allowed.
+ */
+ public static boolean isSchemaSearchableByCaller(
+ @NonNull CallerAccess callerAccess,
+ @NonNull String targetPackageName,
+ @NonNull String prefixedSchema,
+ @Nullable VisibilityStore visibilityStore,
+ @Nullable VisibilityChecker visibilityChecker) {
+ Preconditions.checkNotNull(callerAccess);
+ Preconditions.checkNotNull(targetPackageName);
+ Preconditions.checkNotNull(prefixedSchema);
+
+ if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
+ return true; // Everyone is always allowed to retrieve their own data.
+ }
+ if (visibilityStore == null || visibilityChecker == null) {
+ return false; // No visibility is configured at this time; no other access possible.
+ }
+ return visibilityChecker.isSchemaSearchableByCaller(
+ callerAccess,
+ targetPackageName,
+ prefixedSchema,
+ visibilityStore);
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/api/current.txt b/appsearch/appsearch-platform-storage/api/current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/current.txt
+++ b/appsearch/appsearch-platform-storage/api/current.txt
@@ -2,8 +2,10 @@
package androidx.appsearch.platformstorage {
@RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
}
public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
@@ -2,8 +2,10 @@
package androidx.appsearch.platformstorage {
@RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
}
public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/api/restricted_current.txt b/appsearch/appsearch-platform-storage/api/restricted_current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-platform-storage/api/restricted_current.txt
@@ -2,8 +2,10 @@
package androidx.appsearch.platformstorage {
@RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
- method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+ method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
}
public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/build.gradle b/appsearch/appsearch-platform-storage/build.gradle
index 73bb4b7..e4c1f37 100644
--- a/appsearch/appsearch-platform-storage/build.gradle
+++ b/appsearch/appsearch-platform-storage/build.gradle
@@ -25,8 +25,9 @@
api("androidx.annotation:annotation:1.1.0")
implementation project(":appsearch:appsearch")
+ implementation('androidx.collection:collection:1.2.0')
implementation("androidx.concurrent:concurrent-futures:1.0.0")
- implementation("androidx.core:core:1.2.0")
+ implementation("androidx.core:core:1.7.0")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
diff --git a/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java
new file mode 100644
index 0000000..3ee507c
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.platformstorage.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build;
+
+import androidx.appsearch.app.AppSearchResult;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+
+public class AppSearchResultToPlatformConverterTest {
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public void testPlatformAppSearchResultToJetpack_catchException() {
+ android.app.appsearch.AppSearchResult<String> platformResult =
+ android.app.appsearch.AppSearchResult.newSuccessfulResult("42");
+ AppSearchResult<Integer> jetpackResult =
+ AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
+ platformResult,
+ platformValue -> {
+ throw new IllegalArgumentException("Test exception");
+ }
+ );
+ assertThat(jetpackResult.getResultCode())
+ .isEqualTo(AppSearchResult.RESULT_INVALID_ARGUMENT);
+ assertThat(jetpackResult.getErrorMessage()).contains("Test exception");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public void testPlatformAppSearchResultToFuture_catchException() {
+ android.app.appsearch.AppSearchResult<String> platformResult =
+ android.app.appsearch.AppSearchResult.newSuccessfulResult("42");
+ ResolvableFuture<Integer> future = ResolvableFuture.create();
+ AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ platformResult,
+ future,
+ platformValue -> {
+ throw new IllegalArgumentException("Test exception");
+ }
+ );
+ ExecutionException e = assertThrows(ExecutionException.class, future::get);
+ assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
+ assertThat(e).hasCauseThat().hasMessageThat().contains("Test exception");
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 34ab614..336b213 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -17,6 +17,7 @@
import androidx.annotation.NonNull;
import androidx.appsearch.app.Features;
+import androidx.core.os.BuildCompat;
/**
* An implementation of {@link Features}. Feature availability is dependent on Android API
@@ -25,16 +26,23 @@
final class FeaturesImpl implements Features {
@Override
+ // TODO(b/201316758): Remove once BuildCompat.isAtLeastT is removed
+ @BuildCompat.PrereleaseSdkCheck
public boolean isFeatureSupported(@NonNull String feature) {
if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
- // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is
- // synced over into service-appsearch.
- return false;
+ return BuildCompat.isAtLeastT();
}
- if (Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER.equals(feature)) {
- // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is
- // synced over into service-appsearch.
- return false;
+ if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
+ return BuildCompat.isAtLeastT();
+ }
+ if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
+ return BuildCompat.isAtLeastT();
+ }
+ if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
+ return BuildCompat.isAtLeastT();
+ }
+ if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
+ return BuildCompat.isAtLeastT();
}
return false;
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
index 0984e13..6ed63ee 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -15,31 +15,48 @@
*/
package androidx.appsearch.platformstorage;
+import android.annotation.SuppressLint;
import android.os.Build;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.app.ReportSystemUsageRequest;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.ObserverSpecToPlatformConverter;
import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
+import androidx.collection.ArrayMap;
import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
* An implementation of {@link androidx.appsearch.app.GlobalSearchSession} which proxies to a
* platform {@link android.app.appsearch.GlobalSearchSession}.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -49,6 +66,11 @@
private final Executor mExecutor;
private final Features mFeatures;
+ // Management of observer callbacks.
+ @GuardedBy("mObserverCallbacksLocked")
+ private final Map<ObserverCallback, android.app.appsearch.observer.ObserverCallback>
+ mObserverCallbacksLocked = new ArrayMap<>();
+
GlobalSearchSessionImpl(
@NonNull android.app.appsearch.GlobalSearchSession platformSession,
@NonNull Executor executor,
@@ -58,6 +80,29 @@
mFeatures = Preconditions.checkNotNull(features);
}
+ @BuildCompat.PrereleaseSdkCheck
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName, @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request) {
+ if (!BuildCompat.isAtLeastT()) {
+ throw new UnsupportedOperationException(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
+ + " is not supported on this AppSearch implementation.");
+ }
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(databaseName);
+ Preconditions.checkNotNull(request);
+ ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+ ResolvableFuture.create();
+ mPlatformSession.getByDocumentId(packageName, databaseName,
+ RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request),
+ mExecutor,
+ new BatchResultCallbackAdapter<>(
+ future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
+ return future;
+ }
+
@Override
@NonNull
public SearchResults search(
@@ -74,7 +119,8 @@
@NonNull
@Override
- public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+ public ListenableFuture<Void> reportSystemUsageAsync(
+ @NonNull ReportSystemUsageRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<Void> future = ResolvableFuture.create();
mPlatformSession.reportSystemUsage(
@@ -85,34 +131,136 @@
return future;
}
+ @BuildCompat.PrereleaseSdkCheck
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+ @NonNull String databaseName) {
+ // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+ // unsupported build.
+ if (!BuildCompat.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
+ + " is not supported on this AppSearch implementation.");
+ }
+ ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
+ mPlatformSession.getSchema(
+ packageName,
+ databaseName,
+ mExecutor,
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result,
+ future,
+ GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
+ return future;
+ }
+
@NonNull
@Override
public Features getFeatures() {
return mFeatures;
}
+ // TODO(b/193494000): Remove these two lines once BuildCompat.isAtLeastT() is removed.
+ @SuppressLint("NewApi")
+ @BuildCompat.PrereleaseSdkCheck
@Override
- public void addObserver(
- @NonNull String observedPackage,
+ public void registerObserverCallback(
+ @NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer) {
- Preconditions.checkNotNull(observedPackage);
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ Preconditions.checkNotNull(targetPackageName);
Preconditions.checkNotNull(spec);
Preconditions.checkNotNull(executor);
Preconditions.checkNotNull(observer);
- // TODO(b/193494000): Support change notifications in the platform backend once the
- // feature is exposed in the Android SDK.
- throw new UnsupportedOperationException("addObserver not supported for platform yet");
+ // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+ // unsupported build.
+ if (!BuildCompat.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
+ + " is not supported on this AppSearch implementation");
+ }
+
+ synchronized (mObserverCallbacksLocked) {
+ android.app.appsearch.observer.ObserverCallback frameworkCallback =
+ mObserverCallbacksLocked.get(observer);
+ if (frameworkCallback == null) {
+ // No stub is associated with this package and observer, so we must create one.
+ frameworkCallback = new android.app.appsearch.observer.ObserverCallback() {
+ @Override
+ public void onSchemaChanged(
+ @NonNull android.app.appsearch.observer.SchemaChangeInfo
+ platformSchemaChangeInfo) {
+ SchemaChangeInfo jetpackSchemaChangeInfo =
+ ObserverSpecToPlatformConverter.toJetpackSchemaChangeInfo(
+ platformSchemaChangeInfo);
+ observer.onSchemaChanged(jetpackSchemaChangeInfo);
+ }
+
+ @Override
+ public void onDocumentChanged(
+ @NonNull android.app.appsearch.observer.DocumentChangeInfo
+ platformDocumentChangeInfo) {
+ DocumentChangeInfo jetpackDocumentChangeInfo =
+ ObserverSpecToPlatformConverter.toJetpackDocumentChangeInfo(
+ platformDocumentChangeInfo);
+ observer.onDocumentChanged(jetpackDocumentChangeInfo);
+ }
+ };
+ }
+
+ // Regardless of whether this stub was fresh or not, we have to register it again
+ // because the user might be supplying a different spec.
+ try {
+ mPlatformSession.registerObserverCallback(
+ targetPackageName,
+ ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec),
+ executor,
+ frameworkCallback);
+ } catch (android.app.appsearch.exceptions.AppSearchException e) {
+ throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
+ }
+
+ // Now that registration has succeeded, save this stub into our in-memory cache. This
+ // isn't done when errors occur because the user may not call removeObserver if
+ // addObserver threw.
+ mObserverCallbacksLocked.put(observer, frameworkCallback);
+ }
}
+ @SuppressLint("NewApi")
+ @BuildCompat.PrereleaseSdkCheck
@Override
- public void removeObserver(
- @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
- Preconditions.checkNotNull(observedPackage);
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+ throws AppSearchException {
+ Preconditions.checkNotNull(targetPackageName);
Preconditions.checkNotNull(observer);
- // TODO(b/193494000): Implement removeObserver
- throw new UnsupportedOperationException("removeObserver not supported for platform yet");
+ // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+ // unsupported build.
+ if (!BuildCompat.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
+ + " is not supported on this AppSearch implementation");
+ }
+
+ android.app.appsearch.observer.ObserverCallback frameworkCallback;
+ synchronized (mObserverCallbacksLocked) {
+ frameworkCallback = mObserverCallbacksLocked.get(observer);
+ if (frameworkCallback == null) {
+ return; // No such observer registered. Nothing to do.
+ }
+
+ try {
+ mPlatformSession.unregisterObserverCallback(targetPackageName, frameworkCallback);
+ } catch (android.app.appsearch.exceptions.AppSearchException e) {
+ throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
+ }
+
+ // Only remove from the in-memory map once removal from the service side succeeds
+ mObserverCallbacksLocked.remove(observer);
+ }
}
@Override
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
index ad345db..f7d8c23 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -209,7 +209,7 @@
* {@link AppSearchSession}
*/
@NonNull
- public static ListenableFuture<AppSearchSession> createSearchSession(
+ public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull SearchContext context) {
Preconditions.checkNotNull(context);
AppSearchManager appSearchManager =
@@ -222,7 +222,7 @@
if (result.isSuccess()) {
future.set(
new SearchSessionImpl(result.getResultValue(), context.mExecutor,
- new FeaturesImpl()));
+ new FeaturesImpl()));
} else {
future.setException(
new AppSearchException(
@@ -233,10 +233,23 @@
}
/**
+ * @deprecated use {@link #createSearchSessionAsync}.
+ *
+ * @param context The {@link SearchContext} contains all information to create a new
+ * {@link AppSearchSession}
+ */
+ @NonNull
+ @Deprecated
+ public static ListenableFuture<AppSearchSession> createSearchSession(
+ @NonNull SearchContext context) {
+ return createSearchSessionAsync(context);
+ }
+
+ /**
* Opens a new {@link GlobalSearchSession} on this storage.
*/
@NonNull
- public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+ public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
@NonNull GlobalSearchContext context) {
Preconditions.checkNotNull(context);
AppSearchManager appSearchManager =
@@ -257,4 +270,14 @@
});
return future;
}
+
+ /**
+ * @deprecated use {@link #createGlobalSearchSessionAsync}.
+ */
+ @Deprecated
+ @NonNull
+ public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+ @NonNull GlobalSearchContext context) {
+ return createGlobalSearchSessionAsync(context);
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
index f7390c8..85d6567 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -26,6 +26,7 @@
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter;
import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -58,23 +59,28 @@
@Override
@NonNull
- public ListenableFuture<List<SearchResult>> getNextPage() {
+ @BuildCompat.PrereleaseSdkCheck
+ public ListenableFuture<List<SearchResult>> getNextPageAsync() {
ResolvableFuture<List<SearchResult>> future = ResolvableFuture.create();
mPlatformResults.getNextPage(mExecutor, result -> {
if (result.isSuccess()) {
List<android.app.appsearch.SearchResult> frameworkResults = result.getResultValue();
List<SearchResult> jetpackResults = new ArrayList<>(frameworkResults.size());
for (int i = 0; i < frameworkResults.size(); i++) {
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+ || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) {
// This is a patch for b/197361770, framework-appsearch in Android S will
// disable the whole namespace filter if none of given namespaces exist.
- // And that will result in Icing return all documents this query is able
- // to access.
+ // And that will result in Icing returns all documents that this query is
+ // able to access.
if (i == 0 && !mSearchSpec.getFilterNamespaces().isEmpty()
&& !mSearchSpec.getFilterNamespaces().contains(
frameworkResults.get(i).getGenericDocument().getNamespace())) {
- // And in the meantime, since none of the namespace and document that
- // use query for exists, we should just return an empty result.
+ // We should never return a document with a namespace that is not
+ // required in the request. And also since the bug will only happen
+ // when the required namespace doesn't exist, we should just return
+ // an empty result when we found the result contains unexpected
+ // namespace.
future.set(Collections.emptyList());
return;
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 4d8f2f2..db93018 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -37,13 +37,14 @@
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
import androidx.appsearch.platformstorage.converter.ResponseToPlatformConverter;
-import androidx.appsearch.platformstorage.converter.SchemaToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -75,71 +76,49 @@
@Override
@NonNull
- public ListenableFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest request) {
+ @BuildCompat.PrereleaseSdkCheck
+ public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
mPlatformSession.setSchema(
SetSchemaRequestToPlatformConverter.toPlatformSetSchemaRequest(request),
mExecutor,
mExecutor,
- result -> {
- if (result.isSuccess()) {
- SetSchemaResponse jetpackResponse =
- SetSchemaRequestToPlatformConverter.toJetpackSetSchemaResponse(
- result.getResultValue());
- future.set(jetpackResponse);
- } else {
- handleFailedPlatformResult(result, future);
- }
- });
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result,
+ future,
+ SetSchemaRequestToPlatformConverter::toJetpackSetSchemaResponse));
return future;
}
@Override
@NonNull
- public ListenableFuture<GetSchemaResponse> getSchema() {
+ @BuildCompat.PrereleaseSdkCheck
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
mPlatformSession.getSchema(
mExecutor,
- result -> {
- if (result.isSuccess()) {
- android.app.appsearch.GetSchemaResponse platformGetResponse =
- result.getResultValue();
- GetSchemaResponse.Builder jetpackResponseBuilder =
- new GetSchemaResponse.Builder();
- for (android.app.appsearch.AppSearchSchema platformSchema :
- platformGetResponse.getSchemas()) {
- jetpackResponseBuilder.addSchema(
- SchemaToPlatformConverter.toJetpackSchema(platformSchema));
- }
- jetpackResponseBuilder.setVersion(platformGetResponse.getVersion());
- future.set(jetpackResponseBuilder.build());
- } else {
- handleFailedPlatformResult(result, future);
- }
- });
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result,
+ future,
+ GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
return future;
}
@NonNull
@Override
- public ListenableFuture<Set<String>> getNamespaces() {
+ public ListenableFuture<Set<String>> getNamespacesAsync() {
ResolvableFuture<Set<String>> future = ResolvableFuture.create();
mPlatformSession.getNamespaces(
mExecutor,
- result -> {
- if (result.isSuccess()) {
- future.set(result.getResultValue());
- } else {
- handleFailedPlatformResult(result, future);
- }
- });
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result, future));
return future;
}
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+ public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
@NonNull PutDocumentsRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
@@ -152,7 +131,7 @@
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
@NonNull GetByDocumentIdRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
@@ -181,7 +160,7 @@
@Override
@NonNull
- public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+ public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<Void> future = ResolvableFuture.create();
mPlatformSession.reportUsage(
@@ -194,7 +173,7 @@
@Override
@NonNull
- public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+ public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
@NonNull RemoveByDocumentIdRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
@@ -207,14 +186,14 @@
@Override
@NonNull
- public ListenableFuture<Void> remove(
+ @BuildCompat.PrereleaseSdkCheck
+ public ListenableFuture<Void> removeAsync(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpec);
ResolvableFuture<Void> future = ResolvableFuture.create();
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
- && !searchSpec.getFilterNamespaces().isEmpty()) {
+ if (!BuildCompat.isAtLeastT() && !searchSpec.getFilterNamespaces().isEmpty()) {
// This is a patch for b/197361770, framework-appsearch in Android S will
// disable the given namespace filter if it is not empty and none of given namespaces
// exist.
@@ -223,7 +202,15 @@
mPlatformSession.getNamespaces(
mExecutor,
namespaceResult -> {
- if (namespaceResult.isSuccess()) {
+ if (!namespaceResult.isSuccess()) {
+ future.setException(
+ new AppSearchException(
+ namespaceResult.getResultCode(),
+ namespaceResult.getErrorMessage()));
+ return;
+ }
+
+ try {
Set<String> existingNamespaces = namespaceResult.getResultValue();
List<String> filterNamespaces = searchSpec.getFilterNamespaces();
for (int i = 0; i < filterNamespaces.size(); i++) {
@@ -234,17 +221,18 @@
SearchSpecToPlatformConverter
.toPlatformSearchSpec(searchSpec),
mExecutor,
- removeResult -> AppSearchResultToPlatformConverter
- .platformAppSearchResultToFuture(removeResult,
- future));
+ removeResult ->
+ AppSearchResultToPlatformConverter
+ .platformAppSearchResultToFuture(
+ removeResult, future));
return;
}
}
// None of the namespace in the given namespace filter exists. Return
// early.
future.set(null);
- } else {
- handleFailedPlatformResult(namespaceResult, future);
+ } catch (Throwable t) {
+ future.setException(t);
}
});
} else {
@@ -261,26 +249,19 @@
@Override
@NonNull
- public ListenableFuture<StorageInfo> getStorageInfo() {
+ public ListenableFuture<StorageInfo> getStorageInfoAsync() {
ResolvableFuture<StorageInfo> future = ResolvableFuture.create();
mPlatformSession.getStorageInfo(
mExecutor,
- result -> {
- if (result.isSuccess()) {
- StorageInfo jetpackStorageInfo =
- ResponseToPlatformConverter.toJetpackStorageInfo(
- result.getResultValue());
- future.set(jetpackStorageInfo);
- } else {
- handleFailedPlatformResult(result, future);
- }
- });
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result, future, ResponseToPlatformConverter::toJetpackStorageInfo)
+ );
return future;
}
@NonNull
@Override
- public ListenableFuture<Void> requestFlush() {
+ public ListenableFuture<Void> requestFlushAsync() {
ResolvableFuture<Void> future = ResolvableFuture.create();
// The data in platform will be flushed by scheduled task. This api won't do anything extra
// flush.
@@ -298,12 +279,4 @@
public void close() {
mPlatformSession.close();
}
-
- private void handleFailedPlatformResult(
- @NonNull android.app.appsearch.AppSearchResult<?> platformResult,
- @NonNull ResolvableFuture<?> future) {
- future.setException(
- new AppSearchException(
- platformResult.getResultCode(), platformResult.getErrorMessage()));
- }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
index 1510626..6d9a999 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
@@ -45,11 +45,18 @@
* {@link androidx.appsearch.app.AppSearchResult}.
*/
@NonNull
- public static <T> AppSearchResult<T> platformAppSearchResultToJetpack(
- @NonNull android.app.appsearch.AppSearchResult<T> platformResult) {
+ public static <PlatformType, JetpackType> AppSearchResult<JetpackType>
+ platformAppSearchResultToJetpack(
+ @NonNull android.app.appsearch.AppSearchResult<PlatformType> platformResult,
+ @NonNull Function<PlatformType, JetpackType> valueMapper) {
Preconditions.checkNotNull(platformResult);
if (platformResult.isSuccess()) {
- return AppSearchResult.newSuccessfulResult(platformResult.getResultValue());
+ try {
+ JetpackType jetpackType = valueMapper.apply(platformResult.getResultValue());
+ return AppSearchResult.newSuccessfulResult(jetpackType);
+ } catch (Throwable t) {
+ return AppSearchResult.throwableToFailedResult(t);
+ }
}
return AppSearchResult.newFailedResult(
platformResult.getResultCode(), platformResult.getErrorMessage());
@@ -57,20 +64,36 @@
/**
* Uses the given {@link android.app.appsearch.AppSearchResult} to populate the given
+ * {@link ResolvableFuture}, transforming it using {@code valueMapper}.
+ */
+ public static <PlatformType, JetpackType> void platformAppSearchResultToFuture(
+ @NonNull android.app.appsearch.AppSearchResult<PlatformType> platformResult,
+ @NonNull ResolvableFuture<JetpackType> future,
+ @NonNull Function<PlatformType, JetpackType> valueMapper) {
+ Preconditions.checkNotNull(platformResult);
+ Preconditions.checkNotNull(future);
+ if (platformResult.isSuccess()) {
+ try {
+ JetpackType jetpackType = valueMapper.apply(platformResult.getResultValue());
+ future.set(jetpackType);
+ } catch (Throwable t) {
+ future.setException(t);
+ }
+ } else {
+ future.setException(
+ new AppSearchException(
+ platformResult.getResultCode(), platformResult.getErrorMessage()));
+ }
+ }
+
+ /**
+ * Uses the given {@link android.app.appsearch.AppSearchResult} to populate the given
* {@link ResolvableFuture}.
*/
public static <T> void platformAppSearchResultToFuture(
@NonNull android.app.appsearch.AppSearchResult<T> platformResult,
@NonNull ResolvableFuture<T> future) {
- Preconditions.checkNotNull(platformResult);
- Preconditions.checkNotNull(future);
- if (platformResult.isSuccess()) {
- future.set(platformResult.getResultValue());
- } else {
- future.setException(
- new AppSearchException(
- platformResult.getResultCode(), platformResult.getErrorMessage()));
- }
+ platformAppSearchResultToFuture(platformResult, future, Function.identity());
}
/**
@@ -89,8 +112,13 @@
AppSearchBatchResult.Builder<K, JetpackValue> jetpackResult =
new AppSearchBatchResult.Builder<>();
for (Map.Entry<K, PlatformValue> success : platformResult.getSuccesses().entrySet()) {
- JetpackValue jetpackValue = valueMapper.apply(success.getValue());
- jetpackResult.setSuccess(success.getKey(), jetpackValue);
+ try {
+ JetpackValue jetpackValue = valueMapper.apply(success.getValue());
+ jetpackResult.setSuccess(success.getKey(), jetpackValue);
+ } catch (Throwable t) {
+ jetpackResult.setResult(
+ success.getKey(), AppSearchResult.throwableToFailedResult(t));
+ }
}
for (Map.Entry<K, android.app.appsearch.AppSearchResult<PlatformValue>> failure :
platformResult.getFailures().entrySet()) {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
index 3838191..004fcc4 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -61,9 +61,24 @@
} else if (property instanceof boolean[]) {
platformBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
} else if (property instanceof byte[][]) {
- platformBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+ byte[][] byteValues = (byte[][]) property;
+ // This is a patch for b/204677124, framework-appsearch in Android S and S_V2 will
+ // crash if the user put a document with empty byte[][] or document[].
+ if ((Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+ || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2)
+ && byteValues.length == 0) {
+ continue;
+ }
+ platformBuilder.setPropertyBytes(propertyName, byteValues);
} else if (property instanceof GenericDocument[]) {
GenericDocument[] documentValues = (GenericDocument[]) property;
+ // This is a patch for b/204677124, framework-appsearch in Android S and S_V2 will
+ // crash if the user put a document with empty byte[][] or document[].
+ if ((Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+ || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2)
+ && documentValues.length == 0) {
+ continue;
+ }
android.app.appsearch.GenericDocument[] platformSubDocuments =
new android.app.appsearch.GenericDocument[documentValues.length];
for (int j = 0; j < documentValues.length; j++) {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
new file mode 100644
index 0000000..b69caca
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Preconditions;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link GetSchemaResponse}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class GetSchemaResponseToPlatformConverter {
+ private GetSchemaResponseToPlatformConverter() {}
+
+ /**
+ * Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
+ * {@link GetSchemaResponse}.
+ */
+ @NonNull
+ @BuildCompat.PrereleaseSdkCheck
+ public static GetSchemaResponse toJetpackGetSchemaResponse(
+ @NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
+ Preconditions.checkNotNull(platformResponse);
+ GetSchemaResponse.Builder jetpackBuilder;
+ if (!BuildCompat.isAtLeastT()) {
+ // Android API level in S-v2 and lower won't have any supported feature.
+ jetpackBuilder = new GetSchemaResponse.Builder(/*getVisibilitySettingSupported=*/false);
+ } else {
+ // The regular builder has all supported features.
+ jetpackBuilder = new GetSchemaResponse.Builder();
+ }
+ for (android.app.appsearch.AppSearchSchema platformSchema : platformResponse.getSchemas()) {
+ jetpackBuilder.addSchema(SchemaToPlatformConverter.toJetpackSchema(platformSchema));
+ }
+ jetpackBuilder.setVersion(platformResponse.getVersion());
+ if (BuildCompat.isAtLeastT()) {
+ // Convert schemas not displayed by system
+ for (String schemaTypeNotDisplayedBySystem :
+ platformResponse.getSchemaTypesNotDisplayedBySystem()) {
+ jetpackBuilder.addSchemaTypeNotDisplayedBySystem(schemaTypeNotDisplayedBySystem);
+ }
+ // Convert schemas visible to packages
+ convertSchemasVisibleToPackages(platformResponse, jetpackBuilder);
+ // Convert schemas visible to permissions
+ for (Map.Entry<String, Set<Set<Integer>>> entry :
+ platformResponse.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
+ jetpackBuilder.setRequiredPermissionsForSchemaTypeVisibility(entry.getKey(),
+ entry.getValue());
+ }
+ }
+ return jetpackBuilder.build();
+ }
+
+ /**
+ * Adds package visibilities in a platform {@link android.app.appsearch.GetSchemaResponse} into
+ * the given jetpack {@link GetSchemaResponse}.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private static void convertSchemasVisibleToPackages(
+ @NonNull android.app.appsearch.GetSchemaResponse platformResponse,
+ @NonNull GetSchemaResponse.Builder jetpackBuilder) {
+ // TODO(b/205749173): If there were no packages, getSchemaTypesVisibleToPackages
+ // incorrectly returns {@code null} in some prerelease versions of Android T. Remove
+ // this workaround after the issue is fixed in T.
+ Map<String, Set<android.app.appsearch.PackageIdentifier>> schemaTypesVisibleToPackages =
+ platformResponse.getSchemaTypesVisibleToPackages();
+ if (schemaTypesVisibleToPackages != null) {
+ for (Map.Entry<String, Set<android.app.appsearch.PackageIdentifier>> entry
+ : schemaTypesVisibleToPackages.entrySet()) {
+ Set<PackageIdentifier> jetpackPackageIdentifiers =
+ new ArraySet<>(entry.getValue().size());
+ for (android.app.appsearch.PackageIdentifier frameworkPackageIdentifier
+ : entry.getValue()) {
+ jetpackPackageIdentifiers.add(new PackageIdentifier(
+ frameworkPackageIdentifier.getPackageName(),
+ frameworkPackageIdentifier.getSha256Certificate()));
+ }
+ jetpackBuilder.setSchemaTypeVisibleToPackages(
+ entry.getKey(), jetpackPackageIdentifiers);
+ }
+ }
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java
new file mode 100644
index 0000000..41ca827
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link ObserverSpec}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public final class ObserverSpecToPlatformConverter {
+ private ObserverSpecToPlatformConverter() {}
+
+ /**
+ * Translates a jetpack {@link ObserverSpec} into a platform
+ * {@link android.app.appsearch.observer.ObserverSpec}.
+ */
+ @NonNull
+ public static android.app.appsearch.observer.ObserverSpec toPlatformObserverSpec(
+ @NonNull ObserverSpec jetpackSpec) {
+ Preconditions.checkNotNull(jetpackSpec);
+ return new android.app.appsearch.observer.ObserverSpec.Builder()
+ .addFilterSchemas(jetpackSpec.getFilterSchemas())
+ .build();
+ }
+
+ /**
+ * Translates a platform {@link androidx.appsearch.observer.SchemaChangeInfo} into a jetpack
+ * {@link SchemaChangeInfo}.
+ */
+ @NonNull
+ public static SchemaChangeInfo toJetpackSchemaChangeInfo(
+ @NonNull android.app.appsearch.observer.SchemaChangeInfo platformInfo) {
+ Preconditions.checkNotNull(platformInfo);
+ return new SchemaChangeInfo(
+ platformInfo.getPackageName(),
+ platformInfo.getDatabaseName(),
+ platformInfo.getChangedSchemaNames());
+ }
+
+ /**
+ * Translates a platform {@link androidx.appsearch.observer.DocumentChangeInfo} into a jetpack
+ * {@link DocumentChangeInfo}.
+ */
+ @NonNull
+ public static DocumentChangeInfo toJetpackDocumentChangeInfo(
+ @NonNull android.app.appsearch.observer.DocumentChangeInfo platformInfo) {
+ Preconditions.checkNotNull(platformInfo);
+ return new DocumentChangeInfo(
+ platformInfo.getPackageName(),
+ platformInfo.getDatabaseName(),
+ platformInfo.getNamespace(),
+ platformInfo.getSchemaName(),
+ platformInfo.getChangedDocumentIds());
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
index 841cbe7..707234d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -23,6 +23,7 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.SearchResult;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.List;
@@ -37,6 +38,7 @@
private SearchResultToPlatformConverter() {}
/** Translates from Platform to Jetpack versions of {@link SearchResult}. */
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static SearchResult toJetpackSearchResult(
@NonNull android.app.appsearch.SearchResult platformResult) {
@@ -56,13 +58,13 @@
return builder.build();
}
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
private static SearchResult.MatchInfo toJetpackMatchInfo(
@NonNull android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
Preconditions.checkNotNull(platformMatchInfo);
- // TODO(b/201316758) : Copy over submatch range info once it is added to
- // framework-appsearch.
- return new SearchResult.MatchInfo.Builder(platformMatchInfo.getPropertyPath())
+ SearchResult.MatchInfo.Builder builder = new SearchResult.MatchInfo.Builder(
+ platformMatchInfo.getPropertyPath())
.setExactMatchRange(
new SearchResult.MatchRange(
platformMatchInfo.getExactMatchRange().getStart(),
@@ -70,7 +72,13 @@
.setSnippetRange(
new SearchResult.MatchRange(
platformMatchInfo.getSnippetRange().getStart(),
- platformMatchInfo.getSnippetRange().getEnd()))
- .build();
+ platformMatchInfo.getSnippetRange().getEnd()));
+ if (BuildCompat.isAtLeastT()) {
+ builder.setSubmatchRange(
+ new SearchResult.MatchRange(
+ platformMatchInfo.getSubmatchRange().getStart(),
+ platformMatchInfo.getSubmatchRange().getEnd()));
+ }
+ return builder.build();
}
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
index ca52112..6950427 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
@@ -27,10 +27,12 @@
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
/**
* Translates between Platform and Jetpack versions of {@link SetSchemaRequest}.
@@ -45,6 +47,7 @@
* Translates a jetpack {@link SetSchemaRequest} into a platform
* {@link android.app.appsearch.SetSchemaRequest}.
*/
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
@NonNull SetSchemaRequest jetpackRequest) {
@@ -70,6 +73,20 @@
jetpackPackageIdentifier.getSha256Certificate()));
}
}
+ if (!jetpackRequest.getRequiredPermissionsForSchemaTypeVisibility().isEmpty()) {
+ if (!BuildCompat.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ "Set required permissions for schema type visibility are not supported "
+ + "with this backend/Android API level combination.");
+ }
+ for (Map.Entry<String, Set<Set<Integer>>> entry :
+ jetpackRequest.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
+ for (Set<Integer> permissionGroup : entry.getValue()) {
+ platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(
+ entry.getKey(), permissionGroup);
+ }
+ }
+ }
for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
Migrator jetpackMigrator = entry.getValue();
android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
@@ -141,7 +158,8 @@
migrationFailure.getDocumentId(),
migrationFailure.getSchemaType(),
AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
- migrationFailure.getAppSearchResult())));
+ migrationFailure.getAppSearchResult(), Function.identity()))
+ );
}
return jetpackBuilder.build();
}
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
index f578822..3c2943e 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
@@ -27,6 +27,7 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
import java.util.ArrayList;
import java.util.List;
@@ -59,7 +60,7 @@
@NonNull AppSearchSession session, @NonNull String namespace, @NonNull String... ids)
throws Exception {
AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
- session.getByDocumentId(
+ session.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
assertThat(result.getSuccesses()).hasSize(ids.length);
assertThat(result.getFailures()).isEmpty();
@@ -76,7 +77,7 @@
@NonNull AppSearchSession session, @NonNull GetByDocumentIdRequest request)
throws Exception {
AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
- session.getByDocumentId(request));
+ session.getByDocumentIdAsync(request));
Set<String> ids = request.getIds();
assertThat(result.getSuccesses()).hasSize(ids.size());
assertThat(result.getFailures()).isEmpty();
@@ -104,12 +105,24 @@
@NonNull
public static List<SearchResult> retrieveAllSearchResults(@NonNull SearchResults searchResults)
throws Exception {
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
List<SearchResult> results = new ArrayList<>();
while (!page.isEmpty()) {
results.addAll(page);
- page = searchResults.getNextPage().get();
+ page = searchResults.getNextPageAsync().get();
}
return results;
}
+
+ /**
+ * Creates a mock {@link VisibilityChecker}.
+ * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
+ * @return
+ */
+ @NonNull
+ public static VisibilityChecker createMockVisibilityChecker(
+ @NonNull Set<String> visiblePrefixedSchemas) {
+ return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
+ visiblePrefixedSchemas.contains(prefixedSchema);
+ }
}
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
index 93cb2ce..d1dd006 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
@@ -19,15 +19,15 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.appsearch.observer.AppSearchObserverCallback;
import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.SchemaChangeInfo;
import java.util.ArrayList;
import java.util.List;
/**
- * An implementation of {@link androidx.appsearch.observer.AppSearchObserverCallback} for testing
+ * An implementation of {@link androidx.appsearch.observer.ObserverCallback} for testing
* that caches its notifications in memory.
*
* <p>You should wait for all notifications to be delivered using {@link #waitForNotificationCount}
@@ -36,7 +36,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class TestObserverCallback implements AppSearchObserverCallback {
+public class TestObserverCallback implements ObserverCallback {
private final Object mLock = new Object();
private final List<SchemaChangeInfo> mSchemaChanges = new ArrayList<>();
@@ -111,6 +111,16 @@
return mDocumentChanges;
}
+ /** Removes all notifications captured by this callback and resets the count to 0. */
+ public void clear() {
+ synchronized (mLock) {
+ mSchemaChanges.clear();
+ mDocumentChanges.clear();
+ mNotificationCountLocked = 0;
+ mLock.notifyAll();
+ }
+ }
+
private void incrementNotificationCountLocked() {
synchronized (mLock) {
mNotificationCountLocked++;
diff --git a/appsearch/appsearch/api/api_lint.ignore b/appsearch/appsearch/api/api_lint.ignore
index 36bb389..aa14c59 100644
--- a/appsearch/appsearch/api/api_lint.ignore
+++ b/appsearch/appsearch/api/api_lint.ignore
@@ -19,6 +19,8 @@
Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
AsyncSuffixFuture: androidx.appsearch.app.AppSearchSession#setSchema(androidx.appsearch.app.SetSchemaRequest):
Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
+AsyncSuffixFuture: androidx.appsearch.app.GlobalSearchSession#getSchema(String, String):
+ Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
AsyncSuffixFuture: androidx.appsearch.app.GlobalSearchSession#reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest):
Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
AsyncSuffixFuture: androidx.appsearch.app.SearchResults#getNextPage():
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -177,18 +177,28 @@
public interface AppSearchSession extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
public interface Features {
method public boolean isFeatureSupported(String);
- field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+ field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+ field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
}
@@ -266,6 +279,9 @@
}
public final class GetSchemaResponse {
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method @IntRange(from=0) public int getVersion();
}
@@ -273,17 +289,24 @@
public static final class GetSchemaResponse.Builder {
ctor public GetSchemaResponse.Builder();
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
method public androidx.appsearch.app.GetSchemaResponse build();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
}
public interface GlobalSearchSession extends java.io.Closeable {
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
method public void close();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
}
public abstract class Migrator {
@@ -395,7 +418,8 @@
public interface SearchResults extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
}
public final class SearchSpec {
@@ -453,20 +477,31 @@
public final class SetSchemaRequest {
method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+ method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
method @IntRange(from=1) public int getVersion();
method public boolean isForceOverride();
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
}
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
method public androidx.appsearch.app.SetSchemaRequest build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
@@ -535,19 +570,20 @@
package androidx.appsearch.observer {
- public interface AppSearchObserverCallback {
- method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
- method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
- }
-
public final class DocumentChangeInfo {
- ctor public DocumentChangeInfo(String, String, String, String);
+ ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedDocumentIds();
method public String getDatabaseName();
method public String getNamespace();
method public String getPackageName();
method public String getSchemaName();
}
+ public interface ObserverCallback {
+ method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+ method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+ }
+
public final class ObserverSpec {
method public java.util.Set<java.lang.String!> getFilterSchemas();
}
@@ -562,7 +598,8 @@
}
public final class SchemaChangeInfo {
- ctor public SchemaChangeInfo(String, String);
+ ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedSchemaNames();
method public String getDatabaseName();
method public String getPackageName();
}
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -177,18 +177,28 @@
public interface AppSearchSession extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
public interface Features {
method public boolean isFeatureSupported(String);
- field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+ field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+ field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
}
@@ -266,6 +279,9 @@
}
public final class GetSchemaResponse {
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method @IntRange(from=0) public int getVersion();
}
@@ -273,17 +289,24 @@
public static final class GetSchemaResponse.Builder {
ctor public GetSchemaResponse.Builder();
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
method public androidx.appsearch.app.GetSchemaResponse build();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
}
public interface GlobalSearchSession extends java.io.Closeable {
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
method public void close();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
}
public abstract class Migrator {
@@ -395,7 +418,8 @@
public interface SearchResults extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
}
public final class SearchSpec {
@@ -453,20 +477,31 @@
public final class SetSchemaRequest {
method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+ method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
method @IntRange(from=1) public int getVersion();
method public boolean isForceOverride();
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
}
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
method public androidx.appsearch.app.SetSchemaRequest build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
@@ -535,19 +570,20 @@
package androidx.appsearch.observer {
- public interface AppSearchObserverCallback {
- method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
- method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
- }
-
public final class DocumentChangeInfo {
- ctor public DocumentChangeInfo(String, String, String, String);
+ ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedDocumentIds();
method public String getDatabaseName();
method public String getNamespace();
method public String getPackageName();
method public String getSchemaName();
}
+ public interface ObserverCallback {
+ method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+ method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+ }
+
public final class ObserverSpec {
method public java.util.Set<java.lang.String!> getFilterSchemas();
}
@@ -562,7 +598,8 @@
}
public final class SchemaChangeInfo {
- ctor public SchemaChangeInfo(String, String);
+ ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedSchemaNames();
method public String getDatabaseName();
method public String getPackageName();
}
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -177,18 +177,28 @@
public interface AppSearchSession extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
public interface Features {
method public boolean isFeatureSupported(String);
- field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+ field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+ field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+ field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
}
@@ -266,6 +279,9 @@
}
public final class GetSchemaResponse {
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method @IntRange(from=0) public int getVersion();
}
@@ -273,17 +289,24 @@
public static final class GetSchemaResponse.Builder {
ctor public GetSchemaResponse.Builder();
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
method public androidx.appsearch.app.GetSchemaResponse build();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
}
public interface GlobalSearchSession extends java.io.Closeable {
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
method public void close();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
method public androidx.appsearch.app.Features getFeatures();
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
}
public abstract class Migrator {
@@ -395,7 +418,8 @@
public interface SearchResults extends java.io.Closeable {
method public void close();
- method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
}
public final class SearchSpec {
@@ -453,20 +477,31 @@
public final class SetSchemaRequest {
method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+ method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
method @IntRange(from=1) public int getVersion();
method public boolean isForceOverride();
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+ field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
}
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
method public androidx.appsearch.app.SetSchemaRequest build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
@@ -535,19 +570,20 @@
package androidx.appsearch.observer {
- public interface AppSearchObserverCallback {
- method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
- method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
- }
-
public final class DocumentChangeInfo {
- ctor public DocumentChangeInfo(String, String, String, String);
+ ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedDocumentIds();
method public String getDatabaseName();
method public String getNamespace();
method public String getPackageName();
method public String getSchemaName();
}
+ public interface ObserverCallback {
+ method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+ method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+ }
+
public final class ObserverSpec {
method public java.util.Set<java.lang.String!> getFilterSchemas();
}
@@ -562,7 +598,8 @@
}
public final class SchemaChangeInfo {
- ctor public SchemaChangeInfo(String, String);
+ ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+ method public java.util.Set<java.lang.String!> getChangedSchemaNames();
method public String getDatabaseName();
method public String getPackageName();
}
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index c536467..02e32f9 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -32,9 +32,9 @@
api('androidx.annotation:annotation:1.1.0')
api(libs.guavaListenableFuture)
+ implementation('androidx.collection:collection:1.2.0')
implementation('androidx.concurrent:concurrent-futures:1.0.0')
- implementation('androidx.core:core:1.2.0')
- implementation('androidx.collection:collection:1.0.0')
+ implementation('androidx.core:core:1.7.0')
androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
androidTestImplementation project(':appsearch:appsearch-local-storage')
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
index b22f8d8..6cf35bb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
@@ -26,9 +26,9 @@
public class AnnotationProcessorLocalTest extends AnnotationProcessorTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, dbName).build());
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
index 5dbb8a2..e53c6aa 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
@@ -29,9 +29,9 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AnnotationProcessorPlatformTest extends AnnotationProcessorTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createSearchSession(
+ return PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, dbName).build());
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 684e51f..82c0ae8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -41,12 +41,12 @@
private AppSearchSession mSession;
private static final String DB_NAME_1 = "";
- protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+ protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName);
@Before
public void setUp() throws Exception {
- mSession = createSearchSession(DB_NAME_1).get();
+ mSession = createSearchSessionAsync(DB_NAME_1).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
@@ -60,7 +60,8 @@
}
private void cleanup() throws Exception {
- mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).build()).get();
}
@Document
@@ -297,7 +298,7 @@
public void testAnnotationProcessor() throws Exception {
//TODO(b/156296904) add test for int, float, GenericDocument, and class with
// @Document annotation
- mSession.setSchema(
+ mSession.setSchemaAsync(
new SetSchemaRequest.Builder().addDocumentClasses(Card.class, Gift.class).build())
.get();
@@ -305,7 +306,7 @@
Gift inputDocument = Gift.createPopulatedGift();
// Index the Gift document and query it.
- checkIsBatchResultSuccess(mSession.put(
+ checkIsBatchResultSuccess(mSession.putAsync(
new PutDocumentsRequest.Builder().addDocuments(inputDocument).build()));
SearchResults searchResults = mSession.search("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
@@ -320,7 +321,7 @@
@Test
public void testAnnotationProcessor_queryByType() throws Exception {
- mSession.setSchema(
+ mSession.setSchemaAsync(
new SetSchemaRequest.Builder()
.addDocumentClasses(Card.class, Gift.class)
.addSchemas(AppSearchEmail.SCHEMA).build())
@@ -340,7 +341,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mSession.put(
+ checkIsBatchResultSuccess(mSession.putAsync(
new PutDocumentsRequest.Builder()
.addDocuments(inputDocument1, inputDocument2)
.addGenericDocuments(email1).build()));
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
similarity index 93%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
index 125e55f..12393de 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
@@ -22,7 +22,8 @@
import org.junit.Test;
-public class AppSearchResultTest {
+/** Tests for private APIs of {@link AppSearchResult}. */
+public class AppSearchResultInternalTest {
@Test
public void testMapNullPointerException() {
NullPointerException e = assertThrows(NullPointerException.class, () -> {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
similarity index 95%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index c15f290..4d5b6c6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -23,7 +23,8 @@
import org.junit.Test;
-public class GenericDocumentTest {
+/** Tests for private APIs of {@link GenericDocument}. */
+public class GenericDocumentInternalTest {
@Test
public void testRecreateFromParcel() {
GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java
new file mode 100644
index 0000000..f764313
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.app;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+/** Tests for private APIs of {@link GetSchemaResponse}. */
+public class GetSchemaResponseInternalTest {
+ // TODO(b/205749173): Expose this API and move this test to CTS. Without this API, clients can't
+ // write unit tests that check their code in an environment where visibility settings are not
+ // supported.
+ @Test
+ public void testRebuild_noSupportedException() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ GetSchemaResponse.Builder builder =
+ new GetSchemaResponse.Builder(/*getVisibilitySettingSupported=*/false)
+ .setVersion(42).addSchema(schema1);
+
+ GetSchemaResponse original = builder.build();
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> original.getSchemaTypesNotDisplayedBySystem());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> original.getSchemaTypesVisibleToPackages());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> original.getRequiredPermissionsForSchemaTypeVisibility());
+
+ // rebuild will throw same exception
+ GetSchemaResponse rebuild = builder.setVersion(42).build();
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> rebuild.getSchemaTypesNotDisplayedBySystem());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> rebuild.getSchemaTypesVisibleToPackages());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> original.getRequiredPermissionsForSchemaTypeVisibility());
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
similarity index 96%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index 19ccc63..42de090 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -22,7 +22,8 @@
import org.junit.Test;
-public class SearchSpecTest {
+/** Tests for private APIs of {@link SearchSpec}. */
+public class SearchSpecInternalTest {
@Test
public void testGetBundle() {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
similarity index 96%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
index 8aa6148..799584a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
@@ -20,7 +20,8 @@
import org.junit.Test;
-public class SetSchemaResponseTest {
+/** Tests for private APIs of {@link SetSchemaResponse}. */
+public class SetSchemaResponseInternalTest {
@Test
public void testRebuild() {
SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
index 91dacdb..80094af 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
@@ -23,6 +23,14 @@
import org.junit.Test;
public class AppSearchResultCtsTest {
+ @Test
+ public void testNewSuccessfulResult() {
+ AppSearchResult<String> result = AppSearchResult.newSuccessfulResult("String");
+ assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
+ assertThat(result.getResultValue()).isEqualTo("String");
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getErrorMessage()).isNull();
+ }
@Test
public void testResultEquals_identical() {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
index a980a60..59c17f5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
@@ -118,12 +118,12 @@
private AppSearchSession mDb;
- protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+ protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName);
@Before
public void setUp() throws Exception {
- mDb = createSearchSession(DB_NAME).get();
+ mDb = createSearchSessionAsync(DB_NAME).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
@@ -135,13 +135,13 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).build()).get();
GenericDocument doc = new GenericDocument.Builder<>(
"namespace", "id0", "testSchema")
.setPropertyString("subject", "testPut example1")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id0", null);
assertThat(result.getFailures()).isEmpty();
@@ -150,7 +150,7 @@
@After
public void tearDown() throws Exception {
// Cleanup whatever documents may still exist in these databases.
- mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
@Test
@@ -165,7 +165,7 @@
.build())
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
@@ -185,7 +185,7 @@
.build())
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
@@ -198,7 +198,7 @@
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
@@ -212,7 +212,7 @@
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setForceOverride(true)
@@ -226,7 +226,7 @@
AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
@@ -239,7 +239,7 @@
AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setForceOverride(true)
@@ -258,7 +258,7 @@
.build())
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
@@ -277,7 +277,7 @@
.build())
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
@@ -290,7 +290,7 @@
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
.build();
- mDb.setSchema(
+ mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
@@ -304,7 +304,7 @@
.build();
ExecutionException exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(
+ () -> mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setVersion(2) // upgrade version
@@ -319,7 +319,7 @@
.build();
ExecutionException exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(
+ () -> mDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.build()).get());
@@ -342,7 +342,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).build()).get();
GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -354,7 +354,7 @@
.setPropertyString("To", "testTo example2")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null);
assertThat(result.getFailures()).isEmpty();
@@ -406,13 +406,13 @@
};
SetSchemaResponse setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(2) // upgrade version
.build()).get();
// Check the schema has been saved
- assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+ assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -458,7 +458,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -466,7 +466,7 @@
.setPropertyString("To", "testTo example1")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -508,13 +508,13 @@
};
SetSchemaResponse setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(1) // downgrade version
.build()).get();
// Check the schema has been saved
- assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+ assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -546,7 +546,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -554,7 +554,7 @@
.setPropertyString("To", "testTo example1")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -594,7 +594,7 @@
// SetSchema with forceOverride=false
ExecutionException exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(3) // same version
.build()).get());
@@ -602,12 +602,12 @@
// SetSchema with forceOverride=true
SetSchemaResponse setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(3) // same version
.setForceOverride(true).build()).get();
- assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+ assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -632,7 +632,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).setVersion(2).build()).get();
GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -640,7 +640,7 @@
.setPropertyString("To", "testTo example1")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -680,7 +680,7 @@
// SetSchema with forceOverride=false
ExecutionException exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(4) // upgrade version
.build()).get());
@@ -692,14 +692,14 @@
// set the source schema to AppSearch
AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema")
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(schema).setForceOverride(true).build()).get();
// save a doc to the source type
GenericDocument doc = new GenericDocument.Builder<>(
"namespace", "id1", "sourceSchema")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -730,7 +730,7 @@
// SetSchema with forceOverride=false
// Source type exist, destination type doesn't exist.
ExecutionException exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(new SetSchemaRequest.Builder()
+ () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("sourceSchema", migrator_sourceToNowhere)
.setVersion(2).build()) // upgrade version
@@ -742,7 +742,7 @@
// SetSchema with forceOverride=true
// Source type exist, destination type doesn't exist.
exception = assertThrows(ExecutionException.class,
- () -> mDb.setSchema(new SetSchemaRequest.Builder()
+ () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("sourceSchema", migrator_sourceToNowhere)
.setForceOverride(true)
@@ -758,7 +758,7 @@
// set the destination schema to AppSearch
AppSearchSchema destinationSchema =
new AppSearchSchema.Builder("destinationSchema").build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(destinationSchema).setForceOverride(true).build()).get();
Migrator migrator_nowhereToDestination = new Migrator() {
@@ -787,7 +787,7 @@
// no matter force override or not, the migrator won't be invoked
// SetSchema with forceOverride=false
SetSchemaResponse setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToDestination)
.setVersion(2) // upgrade version
@@ -796,7 +796,7 @@
// SetSchema with forceOverride=true
setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToDestination)
.setVersion(2) // upgrade version
@@ -807,7 +807,7 @@
@Test
public void testSchemaMigration_nowhereToNowhere() throws Exception {
// set empty schema
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.setForceOverride(true).build()).get();
Migrator migrator_nowhereToNowhere = new Migrator() {
@Override
@@ -835,7 +835,7 @@
// no matter force override or not, the migrator won't be invoked
// SetSchema with forceOverride=false
SetSchemaResponse setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToNowhere)
.setVersion(2) // upgrade version
@@ -844,7 +844,7 @@
// SetSchema with forceOverride=true
setSchemaResponse =
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToNowhere)
.setVersion(2) // upgrade version
@@ -857,13 +857,13 @@
// set the source schema to AppSearch
AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema")
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(sourceSchema).setForceOverride(true).build()).get();
// save a doc to the source type
GenericDocument doc = new GenericDocument.Builder<>(
"namespace", "id1", "sourceSchema").build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -897,7 +897,7 @@
};
// SetSchema with forceOverride=false and increase overall version
- SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(destinationSchema)
.setMigrator("sourceSchema", migrator)
.setForceOverride(false)
@@ -925,7 +925,7 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(sourceSchema).setForceOverride(true).build()).get();
// save a child and an adult to the Person type
@@ -935,7 +935,7 @@
GenericDocument adultDoc = new GenericDocument.Builder<>(
"namespace", "Person2", "Person")
.setPropertyLong("Age", 36).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(childDoc, adultDoc).build()));
assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
assertThat(result.getFailures()).isEmpty();
@@ -987,7 +987,7 @@
.build();
// SetSchema with forceOverride=false and increase overall version
- SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(adultSchema, childSchema)
.setMigrator("Person", migrator)
.setForceOverride(false)
@@ -1031,7 +1031,7 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(sourceSchemaA, sourceSchemaB).setForceOverride(true).build()).get();
// save 100 docs to each type
@@ -1047,7 +1047,7 @@
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
putRequestBuilder.addGenericDocuments(docInA, docInB);
}
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
putRequestBuilder.build()));
assertThat(result.getFailures()).isEmpty();
@@ -1141,7 +1141,7 @@
};
// SetSchema with forceOverride=false and increase overall version
- SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(destinationSchemaB, destinationSchemaC, destinationSchemaD)
.setMigrator("schemaA", migratorA)
.setMigrator("schemaB", migratorB)
@@ -1280,7 +1280,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(typeA).setForceOverride(true).setVersion(1).build()).get();
// save a doc to version 1.
@@ -1288,13 +1288,13 @@
"namespace", "id", "TypeA")
.setPropertyString("subject", "subject")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
- SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
@@ -1329,7 +1329,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(typeA).setForceOverride(true).setVersion(2).build()).get();
// save a doc to version 2.
@@ -1338,13 +1338,13 @@
.setPropertyString("subject", "subject")
.setPropertyString("body", "bodyFromA")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
- SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
@@ -1375,7 +1375,7 @@
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb.setSchema(new SetSchemaRequest.Builder()
+ mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(typeA).setForceOverride(true).setVersion(3).build()).get();
// save a doc to version 2.
@@ -1384,13 +1384,13 @@
.setPropertyString("subject", "subject")
.setPropertyString("body", "bodyFromB")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
- SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+ SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB");
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB");
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
index 6525ddd..b6821a0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
@@ -27,9 +27,9 @@
public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, dbName).build());
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
index 405d82e..f2b0e7a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
@@ -30,9 +30,9 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AppSearchSchemaMigrationPlatformCtsTest extends AppSearchSchemaMigrationCtsTestBase{
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createSearchSession(
+ return PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, dbName).build());
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 9d817be..8f535d6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -26,6 +26,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.content.Context;
@@ -40,6 +42,7 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.ReportUsageRequest;
@@ -54,6 +57,7 @@
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -62,6 +66,7 @@
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -78,16 +83,16 @@
private AppSearchSession mDb1;
private AppSearchSession mDb2;
- protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+ protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName);
- protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+ protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName, @NonNull ExecutorService executor);
@Before
public void setUp() throws Exception {
- mDb1 = createSearchSession(DB_NAME_1).get();
- mDb2 = createSearchSession(DB_NAME_2).get();
+ mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+ mDb2 = createSearchSessionAsync(DB_NAME_2).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
@@ -101,9 +106,9 @@
}
private void cleanup() throws Exception {
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
@@ -121,19 +126,19 @@
.setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
}
@Test
public void testSetSchema_Failure() throws Exception {
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
.build();
Throwable throwable = assertThrows(ExecutionException.class,
- () -> mDb1.setSchema(new SetSchemaRequest.Builder()
+ () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(emailSchema1).build()).get()).getCause();
assertThat(throwable).isInstanceOf(AppSearchException.class);
AppSearchException exception = (AppSearchException) throwable;
@@ -142,7 +147,7 @@
assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
throwable = assertThrows(ExecutionException.class,
- () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+ () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
assertThat(throwable).isInstanceOf(AppSearchException.class);
exception = (AppSearchException) throwable;
@@ -166,17 +171,17 @@
.build()
).build();
- mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
.setVersion(1).build()).get();
- Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchema().get().getSchemas();
+ Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
assertThat(actualSchemaTypes).containsExactly(schema);
// increase version number
- mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
.setVersion(2).build()).get();
- GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
}
@@ -197,18 +202,18 @@
).build();
// set different version number to different database.
- mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
.setVersion(135).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
.setVersion(246).build()).get();
// check the version has been set correctly.
- GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
- getSchemaResponse = mDb2.getSchema().get();
+ getSchemaResponse = mDb2.getSchemaAsync().get();
assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
}
@@ -217,7 +222,7 @@
@Test
public void testSetSchema_addDocumentClasses() throws Exception {
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addDocumentClasses(EmailDocument.class).build()).get();
}
// @exportToFramework:endStrip()
@@ -256,13 +261,13 @@
SetSchemaRequest request2 = new SetSchemaRequest.Builder()
.addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build();
- mDb1.setSchema(request1).get();
- mDb2.setSchema(request2).get();
+ mDb1.setSchemaAsync(request1).get();
+ mDb2.setSchemaAsync(request2).get();
- Set<AppSearchSchema> actual1 = mDb1.getSchema().get().getSchemas();
+ Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
assertThat(actual1).hasSize(2);
assertThat(actual1).isEqualTo(request1.getSchemas());
- Set<AppSearchSchema> actual2 = mDb2.getSchema().get().getSchemas();
+ Set<AppSearchSchema> actual2 = mDb2.getSchemaAsync().get().getSchemas();
assertThat(actual2).hasSize(2);
assertThat(actual2).isEqualTo(request2.getSchemas());
}
@@ -296,9 +301,9 @@
.build();
// Add it to AppSearch and then obtain it again
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(inSchema, AppSearchEmail.SCHEMA).build()).get();
- GetSchemaResponse response = mDb1.getSchema().get();
+ GetSchemaResponse response = mDb1.getSchemaAsync().get();
List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas());
assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA);
AppSearchSchema outSchema;
@@ -351,87 +356,213 @@
}
@Test
+ public void testGetSchema_visibilitySetting() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ byte[] shar256Cert1 = new byte[32];
+ Arrays.fill(shar256Cert1, (byte) 1);
+ byte[] shar256Cert2 = new byte[32];
+ Arrays.fill(shar256Cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 =
+ new PackageIdentifier("pkgFoo", shar256Cert1);
+ PackageIdentifier packageIdentifier2 =
+ new PackageIdentifier("pkgBar", shar256Cert2);
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(emailSchema)
+ .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+ .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+ packageIdentifier1)
+ .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+ packageIdentifier2)
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+
+ mDb1.setSchemaAsync(request).get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+ assertThat(actual).hasSize(1);
+ assertThat(actual).isEqualTo(request.getSchemas());
+ assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+ .containsExactly("Email1");
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
+ .containsExactly("Email1", ImmutableSet.of(
+ packageIdentifier1, packageIdentifier2));
+ assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly("Email1", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+ }
+
+ @Test
+ public void testGetSchema_visibilitySetting_notSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ byte[] shar256Cert1 = new byte[32];
+ Arrays.fill(shar256Cert1, (byte) 1);
+ byte[] shar256Cert2 = new byte[32];
+ Arrays.fill(shar256Cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 =
+ new PackageIdentifier("pkgFoo", shar256Cert1);
+ PackageIdentifier packageIdentifier2 =
+ new PackageIdentifier("pkgBar", shar256Cert2);
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(emailSchema)
+ .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+ .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+ packageIdentifier1)
+ .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+ packageIdentifier2)
+ .build();
+
+ mDb1.setSchemaAsync(request).get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+ assertThat(actual).hasSize(1);
+ assertThat(actual).isEqualTo(request.getSchemas());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> getSchemaResponse.getSchemaTypesNotDisplayedBySystem());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> getSchemaResponse.getSchemaTypesVisibleToPackages());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility());
+ }
+
+ @Test
+ public void testSetSchema_visibilitySettingPermission_notSupported() {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1").build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(emailSchema)
+ .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS))
+ .build();
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ mDb1.setSchemaAsync(request).get());
+ }
+
+ @Test
public void testGetNamespaces() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- assertThat(mDb1.getNamespaces().get()).isEmpty();
+ assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
// Index a document
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build())
.build()));
- assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1");
+ assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1");
// Index additional data
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(
new AppSearchEmail.Builder("namespace2", "id1").build(),
new AppSearchEmail.Builder("namespace2", "id2").build(),
new AppSearchEmail.Builder("namespace3", "id1").build())
.build()));
- assertThat(mDb1.getNamespaces().get()).containsExactly(
+ assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
"namespace1", "namespace2", "namespace3");
// Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
checkIsBatchResultSuccess(
- mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+ mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
"id2").build()));
- assertThat(mDb1.getNamespaces().get()).containsExactly(
+ assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
"namespace1", "namespace2", "namespace3");
// Remove namespace2/id1 -- namespace2 should now be gone
checkIsBatchResultSuccess(
- mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+ mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
"id1").build()));
- assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+ assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
// Make sure the list of namespaces is preserved after restart
mDb1.close();
- mDb1 = createSearchSession(DB_NAME_1).get();
- assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+ mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+ assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
}
@Test
public void testGetNamespaces_dbIsolation() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- assertThat(mDb1.getNamespaces().get()).isEmpty();
- assertThat(mDb2.getNamespaces().get()).isEmpty();
+ assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
+ assertThat(mDb2.getNamespacesAsync().get()).isEmpty();
// Index documents
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build())
.build()));
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build())
.build()));
- checkIsBatchResultSuccess(mDb2.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb2.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(new AppSearchEmail.Builder("namespace_db2", "id1").build())
.build()));
- assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
- assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+ assertThat(mDb1.getNamespacesAsync().get())
+ .containsExactly("namespace1_db1", "namespace2_db1");
+ assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
// Make sure the list of namespaces is preserved after restart
mDb1.close();
- mDb1 = createSearchSession(DB_NAME_1).get();
- assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
- assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+ mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+ assertThat(mDb1.getNamespacesAsync().get())
+ .containsExactly("namespace1_db1", "namespace2_db1");
+ assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
}
@Test
public void testGetSchema_emptyDB() throws Exception {
- GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
}
@Test
public void testPutDocuments() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -442,18 +573,122 @@
.setBody("This is the body of the testPut email")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
}
+ @Test
+ public void testPutDocuments_emptyProperties() throws Exception {
+ // Schema registration. Due to b/204677124 is fixed in Android T. We have different
+ // behaviour when set empty array to bytes and documents between local and platform storage.
+ // This test only test String, long, boolean and double, for byte array and Document will be
+ // test in backend's specific test.
+ AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+ .addProperty(new StringPropertyConfig.Builder("string")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
+
+ // Index a document
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+ .setPropertyBoolean("boolean")
+ .setPropertyString("string")
+ .setPropertyDouble("double")
+ .setPropertyLong("long")
+ .build();
+
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+ assertThat(result.getSuccesses()).containsExactly("id1", null);
+ assertThat(result.getFailures()).isEmpty();
+
+ GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id1")
+ .build();
+ List<GenericDocument> outDocuments = doGet(mDb1, request);
+ assertThat(outDocuments).hasSize(1);
+ GenericDocument outDocument = outDocuments.get(0);
+ assertThat(outDocument.getPropertyBooleanArray("boolean")).isEmpty();
+ assertThat(outDocument.getPropertyStringArray("string")).isEmpty();
+ assertThat(outDocument.getPropertyDoubleArray("double")).isEmpty();
+ assertThat(outDocument.getPropertyLongArray("long")).isEmpty();
+ }
+
+ @Test
+ public void testPutLargeDocumentBatch() throws Exception {
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Creates a large batch of Documents, since we have max document size in Framework which is
+ // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
+ char[] chars = new char[1024]; // 1KiB
+ Arrays.fill(chars, ' ');
+ String body = String.valueOf(chars) + "the end.";
+ List<GenericDocument> inDocuments = new ArrayList<>();
+ GetByDocumentIdRequest.Builder getByDocumentIdRequestBuilder =
+ new GetByDocumentIdRequest.Builder("namespace");
+ for (int i = 0; i < 4000; i++) {
+ GenericDocument inDocument = new GenericDocument.Builder<>(
+ "namespace", "id" + i, "Type")
+ .setPropertyString("body", body)
+ .build();
+ inDocuments.add(inDocument);
+ getByDocumentIdRequestBuilder.addIds("id" + i);
+ }
+
+ // Index documents.
+ AppSearchBatchResult<String, Void> result =
+ mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(inDocuments)
+ .build()).get();
+ assertThat(result.isSuccess()).isTrue();
+
+ // Query those documents and verify they are same with the input. This also verify
+ // AppSearchResult could handle large batch.
+ SearchResults searchResults = mDb1.search("end", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build());
+ List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults);
+ assertThat(inDocuments).containsExactlyElementsIn(outDocuments);
+
+ // Get by document ID and verify they are same with the input. This also verify
+ // AppSearchBatchResult could handle large batch.
+ AppSearchBatchResult<String, GenericDocument> batchResult = mDb1.getByDocumentIdAsync(
+ getByDocumentIdRequestBuilder.build()).get();
+ assertThat(batchResult.isSuccess()).isTrue();
+ for (int i = 0; i < inDocuments.size(); i++) {
+ GenericDocument inDocument = inDocuments.get(i);
+ assertThat(batchResult.getSuccesses().get(inDocument.getId())).isEqualTo(inDocument);
+ }
+ }
+
// @exportToFramework:startStrip()
@Test
public void testPut_addDocumentClasses() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addDocumentClasses(EmailDocument.class).build()).get();
// Index a document
@@ -463,7 +698,7 @@
email.subject = "testPut example";
email.body = "This is the body of the testPut email";
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addDocuments(email).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
@@ -497,7 +732,7 @@
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
// Try to index a gift. This should fail as it's not in the schema.
@@ -505,19 +740,19 @@
new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price",
5).build();
AppSearchBatchResult<String, Void> result =
- mDb1.put(
+ mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get();
assertThat(result.isSuccess()).isFalse();
assertThat(result.getFailures().get("gift1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Update the schema to include the gift and update email with a new field
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(newEmailSchema, giftSchema).build()).get();
// Try to index the document again, which should now work
checkIsBatchResultSuccess(
- mDb1.put(
+ mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
// Indexing an email with a body should also work
@@ -526,7 +761,7 @@
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
- mDb1.put(
+ mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
}
@@ -540,14 +775,14 @@
.setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
// Index an email and check it present.
AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
.setSubject("testPut example")
.build();
checkIsBatchResultSuccess(
- mDb1.put(
+ mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
List<GenericDocument> outDocuments =
doGet(mDb1, "namespace", "email1");
@@ -558,7 +793,7 @@
// Try to remove the email schema. This should fail as it's an incompatible change.
Throwable failResult1 = assertThrows(
ExecutionException.class,
- () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+ () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
assertThat(failResult1).isInstanceOf(AppSearchException.class);
assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
assertThat(failResult1).hasMessageThat().contains(
@@ -566,10 +801,10 @@
// Try to remove the email schema again, which should now work as we set forceOverride to
// be true.
- mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
// Make sure the indexed email is gone.
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace")
.addIds("email1")
.build()).get();
@@ -581,7 +816,7 @@
AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
.setSubject("testPut example")
.build();
- AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+ AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email2").getErrorMessage())
@@ -599,15 +834,15 @@
.setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
// Index an email and check it present in database1.
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "email1")
.setSubject("testPut example")
.build();
checkIsBatchResultSuccess(
- mDb1.put(
+ mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
List<GenericDocument> outDocuments =
doGet(mDb1, "namespace", "email1");
@@ -620,7 +855,7 @@
.setSubject("testPut example")
.build();
checkIsBatchResultSuccess(
- mDb2.put(
+ mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
outDocuments = doGet(mDb2, "namespace", "email2");
assertThat(outDocuments).hasSize(1);
@@ -631,7 +866,7 @@
// change.
Throwable failResult1 = assertThrows(
ExecutionException.class,
- () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+ () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
assertThat(failResult1).isInstanceOf(AppSearchException.class);
assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
assertThat(failResult1).hasMessageThat().contains(
@@ -639,10 +874,10 @@
// Try to remove the email schema again, which should now work as we set forceOverride to
// be true.
- mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
// Make sure the indexed email is gone in database 1.
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace")
.addIds("email1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
@@ -653,7 +888,7 @@
AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3")
.setSubject("testPut example")
.build();
- AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+ AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email3").getErrorMessage())
@@ -668,14 +903,14 @@
// Make sure email could still be indexed in database 2.
checkIsBatchResultSuccess(
- mDb2.put(
+ mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
}
@Test
public void testGetDocuments() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -686,7 +921,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Get the document
@@ -696,7 +931,7 @@
assertThat(outEmail).isEqualTo(inEmail);
// Can't get the document in the other instance.
- AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds(
"id1").build()).get();
assertThat(failResult.isSuccess()).isFalse();
@@ -709,7 +944,7 @@
@Test
public void testGet_addDocumentClasses() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addDocumentClasses(EmailDocument.class).build()).get();
// Index a document
@@ -718,7 +953,7 @@
inEmail.id = "id1";
inEmail.subject = "testPut example";
inEmail.body = "This is the body of the testPut inEmail";
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addDocuments(inEmail).build()));
// Get the document
@@ -735,7 +970,7 @@
@Test
public void testGetDocuments_projection() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -757,7 +992,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -789,7 +1024,7 @@
@Test
public void testGetDocuments_projectionEmpty() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -811,7 +1046,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -836,7 +1071,7 @@
@Test
public void testGetDocuments_projectionNonExistentType() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -858,7 +1093,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -890,7 +1125,7 @@
@Test
public void testGetDocuments_wildcardProjection() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -912,7 +1147,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -945,7 +1180,7 @@
@Test
public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -967,7 +1202,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -993,7 +1228,7 @@
@Test
public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -1015,7 +1250,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -1049,7 +1284,7 @@
@Test
public void testQuery() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -1060,7 +1295,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
@@ -1083,7 +1318,7 @@
@Test
public void testQuery_getNextPage() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
Set<AppSearchEmail> emailSet = new HashSet<>();
PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
@@ -1099,7 +1334,7 @@
emailSet.add(inEmail);
putDocumentsRequestBuilder.addGenericDocuments(inEmail);
}
- checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+ checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
// Set number of results per page is 7.
SearchResults searchResults = mDb1.search("body",
@@ -1114,7 +1349,7 @@
// keep loading next page until it's empty.
do {
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
++pageNumber;
for (SearchResult result : results) {
documents.add(result.getGenericDocument());
@@ -1129,7 +1364,7 @@
@Test
public void testQuery_relevanceScoring() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -1151,7 +1386,7 @@
.setSubject("I'm a little teapot")
.setBody("short and stout. Here is my handle, here is my spout.")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2).build()));
@@ -1198,7 +1433,7 @@
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(genericSchema)
@@ -1214,7 +1449,7 @@
.build();
GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic")
.setPropertyString("foo", "body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build()));
// Query for the documents
@@ -1246,7 +1481,7 @@
@Test
public void testQuery_packageFilter() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -1257,7 +1492,7 @@
.setSubject("foo")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
// Query for the document within our package
@@ -1273,14 +1508,14 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames("some.other.package")
.build());
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).isEmpty();
}
@Test
public void testQuery_namespaceFilter() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index two documents
@@ -1298,7 +1533,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(expectedEmail, unexpectedEmail).build()));
@@ -1333,7 +1568,7 @@
@Test
public void testQuery_getPackageName() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -1344,7 +1579,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
@@ -1356,7 +1591,7 @@
List<GenericDocument> documents = new ArrayList<>();
// keep loading next page until it's empty.
do {
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
for (SearchResult result : results) {
assertThat(result.getGenericDocument()).isEqualTo(inEmail);
assertThat(result.getPackageName()).isEqualTo(
@@ -1370,7 +1605,7 @@
@Test
public void testQuery_getDatabaseName() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -1381,7 +1616,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
@@ -1393,7 +1628,7 @@
List<GenericDocument> documents = new ArrayList<>();
// keep loading next page until it's empty.
do {
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
for (SearchResult result : results) {
assertThat(result.getGenericDocument()).isEqualTo(inEmail);
assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
@@ -1403,10 +1638,10 @@
assertThat(documents).hasSize(1);
// Schema registration for another database
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
@@ -1417,7 +1652,7 @@
documents = new ArrayList<>();
// keep loading next page until it's empty.
do {
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
for (SearchResult result : results) {
assertThat(result.getGenericDocument()).isEqualTo(inEmail);
assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
@@ -1430,7 +1665,7 @@
@Test
public void testQuery_projection() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1465,7 +1700,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1495,7 +1730,7 @@
@Test
public void testQuery_projectionEmpty() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1530,7 +1765,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1558,7 +1793,7 @@
@Test
public void testQuery_projectionNonExistentType() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1593,7 +1828,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1624,7 +1859,7 @@
@Test
public void testQuery_wildcardProjection() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1658,7 +1893,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1688,7 +1923,7 @@
@Test
public void testQuery_wildcardProjectionEmpty() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1720,7 +1955,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1745,7 +1980,7 @@
@Test
public void testQuery_wildcardProjectionNonExistentType() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("Note")
@@ -1780,7 +2015,7 @@
.setCreationTimestampMillis(1000)
.setPropertyString("title", "Note title")
.setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email, note).build()));
@@ -1811,9 +2046,9 @@
@Test
public void testQuery_twoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document to instance 1.
@@ -1824,7 +2059,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
// Index a document to instance 2.
@@ -1835,7 +2070,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
// Query for instance 1.
@@ -1865,7 +2100,7 @@
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
// Index a document
@@ -1874,7 +2109,7 @@
.setPropertyString("subject", "A commonly used fake word is foo. "
+ "Another nonsense word that’s used a lot is bar")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
// Query for the document
@@ -1886,7 +2121,7 @@
.setMaxSnippetSize(10)
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build());
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(1);
List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
@@ -1923,32 +2158,36 @@
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
// Index documents
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(
- new GenericDocument.Builder<>("namespace", "id1", "Generic")
- .setPropertyString(
- "subject",
- "I like cats", "I like dogs", "I like birds", "I like fish")
- .setScore(10)
- .build(),
- new GenericDocument.Builder<>("namespace", "id2", "Generic")
- .setPropertyString(
- "subject",
- "I like red", "I like green", "I like blue", "I like yellow")
- .setScore(20)
- .build(),
- new GenericDocument.Builder<>("namespace", "id3", "Generic")
- .setPropertyString(
- "subject",
- "I like cupcakes",
- "I like donuts",
- "I like eclairs",
- "I like froyo")
- .setScore(5)
- .build())
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(
+ new GenericDocument.Builder<>("namespace", "id1", "Generic")
+ .setPropertyString(
+ "subject",
+ "I like cats", "I like dogs", "I like birds", "I like fish")
+ .setScore(10)
+ .build(),
+ new GenericDocument.Builder<>("namespace", "id2", "Generic")
+ .setPropertyString(
+ "subject",
+ "I like red",
+ "I like green",
+ "I like blue",
+ "I like yellow")
+ .setScore(20)
+ .build(),
+ new GenericDocument.Builder<>("namespace", "id3", "Generic")
+ .setPropertyString(
+ "subject",
+ "I like cupcakes",
+ "I like donuts",
+ "I like eclairs",
+ "I like froyo")
+ .setScore(5)
+ .build())
.build()));
// Query for the document
@@ -1964,7 +2203,7 @@
.build());
// Check result 1
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(3);
assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2");
@@ -1998,7 +2237,7 @@
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
String japanese =
@@ -2011,7 +2250,7 @@
new GenericDocument.Builder<>("namespace", "id", "Generic")
.setPropertyString("subject", japanese)
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
// Query for the document
@@ -2022,7 +2261,7 @@
.setSnippetCountPerProperty(1)
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build());
- List<SearchResult> results = searchResults.getNextPage().get();
+ List<SearchResult> results = searchResults.getNextPageAsync().get();
assertThat(results).hasSize(1);
List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
@@ -2048,7 +2287,7 @@
@Test
public void testRemove() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2066,7 +2305,7 @@
.setSubject("testPut example 2")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
// Check the presence of the documents
@@ -2074,12 +2313,12 @@
assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
// Delete the document
- checkIsBatchResultSuccess(mDb1.remove(
+ checkIsBatchResultSuccess(mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds(
"id1").build()));
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
"id2").build())
.get();
@@ -2089,7 +2328,7 @@
assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
// Test if we delete a nonexistent id.
- AppSearchBatchResult<String, Void> deleteResult = mDb1.remove(
+ AppSearchBatchResult<String, Void> deleteResult = mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds(
"id1").build()).get();
@@ -2100,7 +2339,7 @@
@Test
public void testRemove_multipleIds() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2118,7 +2357,7 @@
.setSubject("testPut example 2")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
// Check the presence of the documents
@@ -2126,13 +2365,13 @@
assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
// Delete the document
- checkIsBatchResultSuccess(mDb1.remove(
+ checkIsBatchResultSuccess(mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build()));
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
- new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
- "id2").build())
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
+ new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
+ "id2").build())
.get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2144,7 +2383,7 @@
@Test
public void testRemoveByQuery() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2162,7 +2401,7 @@
.setSubject("bar")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
// Check the presence of the documents
@@ -2170,10 +2409,11 @@
assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
// Delete the email 1 by query "foo"
- mDb1.remove("foo",
+ mDb1.removeAsync("foo",
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
- new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
+ new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id1", "id2").build())
.get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2181,10 +2421,10 @@
assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
// Delete the email 2 by query "bar"
- mDb1.remove("bar",
+ mDb1.removeAsync("bar",
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
- getResult = mDb1.getByDocumentId(
- new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
+ getResult = mDb1.getByDocumentIdAsync(
+ new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
.get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id2").getResultCode())
@@ -2195,7 +2435,7 @@
@Test
public void testRemoveByQuery_nonExistNamespace() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2213,7 +2453,7 @@
.setSubject("bar")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
// Check the presence of the documents
@@ -2221,7 +2461,7 @@
assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
// Delete the email by nonExist namespace.
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("nonExistNamespace").build()).get();
// None of these emails will be deleted.
@@ -2232,7 +2472,7 @@
@Test
public void testRemoveByQuery_packageFilter() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2243,7 +2483,7 @@
.setSubject("foo")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
// Check the presence of the documents
@@ -2251,17 +2491,17 @@
// Try to delete email with query "foo", but restricted to a different package name.
// Won't work and email will still exist.
- mDb1.remove("foo",
+ mDb1.removeAsync("foo",
new SearchSpec.Builder().setTermMatch(
SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
"some.other.package").build()).get();
assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
// Delete the email by query "foo", restricted to the correct package this time.
- mDb1.remove("foo", new SearchSpec.Builder().setTermMatch(
+ mDb1.removeAsync("foo", new SearchSpec.Builder().setTermMatch(
SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
ApplicationProvider.getApplicationContext().getPackageName()).build()).get();
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
.get();
assertThat(getResult.isSuccess()).isFalse();
@@ -2272,7 +2512,7 @@
@Test
public void testRemove_twoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2283,32 +2523,32 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
// Check the presence of the documents
assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
// Can't delete in the other instance.
- AppSearchBatchResult<String, Void> deleteResult = mDb2.remove(
+ AppSearchBatchResult<String, Void> deleteResult = mDb2.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
AppSearchResult.RESULT_NOT_FOUND);
assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
// Delete the document
- checkIsBatchResultSuccess(mDb1.remove(
+ checkIsBatchResultSuccess(mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Test if we delete a nonexistent id.
- deleteResult = mDb1.remove(
+ deleteResult = mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
AppSearchResult.RESULT_NOT_FOUND);
@@ -2318,7 +2558,7 @@
public void testRemoveByTypes() throws Exception {
// Schema registration
AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
genericSchema).build()).get();
@@ -2339,7 +2579,7 @@
.build();
GenericDocument document1 =
new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
.build()));
@@ -2347,7 +2587,7 @@
assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
// Delete the email type
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
@@ -2355,7 +2595,7 @@
.get();
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build())
.get();
assertThat(getResult.isSuccess()).isFalse();
@@ -2369,9 +2609,9 @@
@Test
public void testRemoveByTypes_twoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2389,9 +2629,9 @@
.setSubject("testPut example 2")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Check the presence of the documents
@@ -2399,7 +2639,7 @@
assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
// Delete the email type in instance 1
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
@@ -2407,14 +2647,14 @@
.get();
// Make sure it's really gone in instance 1
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Make sure it's still in instance 2.
- getResult = mDb2.getByDocumentId(
+ getResult = mDb2.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
assertThat(getResult.isSuccess()).isTrue();
assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
@@ -2430,7 +2670,7 @@
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).build();
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
genericSchema).build()).get();
@@ -2452,7 +2692,7 @@
GenericDocument document1 =
new GenericDocument.Builder<>("document", "id3", "Generic")
.setPropertyString("foo", "bar").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
.build()));
@@ -2461,7 +2701,7 @@
assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
// Delete the email namespace
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("email")
@@ -2469,7 +2709,7 @@
.get();
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("email")
.addIds("id1", "id2").build()).get();
assertThat(getResult.isSuccess()).isFalse();
@@ -2477,7 +2717,7 @@
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
assertThat(getResult.getFailures().get("id2").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
- getResult = mDb1.getByDocumentId(
+ getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("document")
.addIds("id3").build()).get();
assertThat(getResult.isSuccess()).isTrue();
@@ -2487,9 +2727,9 @@
@Test
public void testRemoveByNamespaces_twoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2507,9 +2747,9 @@
.setSubject("testPut example 2")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Check the presence of the documents
@@ -2517,7 +2757,7 @@
assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
// Delete the email namespace in instance 1
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("email")
@@ -2525,7 +2765,7 @@
.get();
// Make sure it's really gone in instance 1
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("email")
.addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
@@ -2533,7 +2773,7 @@
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Make sure it's still in instance 2.
- getResult = mDb2.getByDocumentId(
+ getResult = mDb2.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("email")
.addIds("id2").build()).get();
assertThat(getResult.isSuccess()).isTrue();
@@ -2543,9 +2783,9 @@
@Test
public void testRemoveAll_twoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2563,9 +2803,9 @@
.setSubject("testPut example 2")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Check the presence of the documents
@@ -2573,21 +2813,21 @@
assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
// Delete the all document in instance 1
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build())
.get();
// Make sure it's really gone in instance 1
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Make sure it's still in instance 2.
- getResult = mDb2.getByDocumentId(
+ getResult = mDb2.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
assertThat(getResult.isSuccess()).isTrue();
assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
@@ -2596,9 +2836,9 @@
@Test
public void testRemoveAll_termMatchType() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2630,9 +2870,9 @@
.setSubject("testPut example 4")
.setBody("This is the body of the testPut second email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build()));
// Check the presence of the documents
@@ -2648,7 +2888,7 @@
assertThat(documents).hasSize(2);
// Delete the all document in instance 1 with TERM_MATCH_PREFIX
- mDb1.remove("",
+ mDb1.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build())
@@ -2660,7 +2900,7 @@
assertThat(documents).isEmpty();
// Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
- mDb2.remove("",
+ mDb2.removeAsync("",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.build())
@@ -2675,7 +2915,7 @@
@Test
public void testRemoveAllAfterEmpty() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -2686,7 +2926,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
// Check the presence of the documents
@@ -2694,23 +2934,22 @@
// Remove the document
checkIsBatchResultSuccess(
- mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+ mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
"id1").build()));
// Make sure it's really gone
- AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+ AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Delete the all documents
- mDb1.remove(
- "", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
- .get();
+ mDb1.removeAsync("", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
// Make sure it's still gone
- getResult = mDb1.getByDocumentId(
+ getResult = mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
assertThat(getResult.isSuccess()).isFalse();
assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2720,7 +2959,7 @@
@Test
public void testCloseAndReopen() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -2731,12 +2970,12 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// close and re-open the appSearchSession
mDb1.close();
- mDb1 = createSearchSession(DB_NAME_1).get();
+ mDb1 = createSearchSessionAsync(DB_NAME_1).get();
// Query for the document
SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
@@ -2752,12 +2991,12 @@
// Create a same-thread database by inject an executor which could help us maintain the
// execution order of those async tasks.
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession sameThreadDb = createSearchSession(
+ AppSearchSession sameThreadDb = createSearchSessionAsync(
"sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
try {
// Schema registration -- just mutate something
- sameThreadDb.setSchema(
+ sameThreadDb.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Close the database. No further call will be allowed.
@@ -2773,15 +3012,16 @@
} finally {
// To clean the data that has been added in the test, need to re-open the session and
// set an empty schema.
- AppSearchSession reopen = createSearchSession(
+ AppSearchSession reopen = createSearchSessionAsync(
"sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
- reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ reopen.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).build()).get();
}
}
@Test
public void testReportUsage() throws Exception {
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index two documents.
@@ -2789,19 +3029,19 @@
new AppSearchEmail.Builder("namespace", "id1").build();
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
// Email 1 has more usages, but email 2 has more recent usages.
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
.setUsageTimestampMillis(1000).build()).get();
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
.setUsageTimestampMillis(2000).build()).get();
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
.setUsageTimestampMillis(3000).build()).get();
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
.setUsageTimestampMillis(10000).build()).get();
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
.setUsageTimestampMillis(20000).build()).get();
// Query by number of usages
@@ -2832,19 +3072,19 @@
@Test
public void testReportUsage_invalidNamespace() throws Exception {
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
// Use the correct namespace; it works
- mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
+ mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
// Use an incorrect namespace; it fails
ExecutionException e = assertThrows(
ExecutionException.class,
- () -> mDb1.reportUsage(
+ () -> mDb1.reportUsageAsync(
new ReportUsageRequest.Builder("namespace2", "id1").build()).get());
assertThat(e).hasCauseThat().isInstanceOf(AppSearchException.class);
AppSearchException cause = (AppSearchException) e.getCause();
@@ -2853,26 +3093,26 @@
@Test
public void testGetStorageInfo() throws Exception {
- StorageInfo storageInfo = mDb1.getStorageInfo().get();
+ StorageInfo storageInfo = mDb1.getStorageInfoAsync().get();
assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Still no storage space attributed with just a schema
- storageInfo = mDb1.getStorageInfo().get();
+ storageInfo = mDb1.getStorageInfoAsync().get();
assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
// Index two documents.
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build();
AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build();
AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2,
email3).build()));
// Non-zero size now
- storageInfo = mDb1.getStorageInfo().get();
+ storageInfo = mDb1.getStorageInfoAsync().get();
assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
@@ -2881,7 +3121,7 @@
@Test
public void testFlush() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -2892,19 +3132,19 @@
.setBody("This is the body of the testPut email")
.build();
- AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
// The future returned from requestFlush will be set as a void or an Exception on error.
- mDb1.requestFlush().get();
+ mDb1.requestFlushAsync().get();
}
@Test
public void testQuery_ResultGroupingLimits() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index four documents.
@@ -2915,7 +3155,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
AppSearchEmail inEmail2 =
new AppSearchEmail.Builder("namespace1", "id2")
@@ -2924,7 +3164,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
AppSearchEmail inEmail3 =
new AppSearchEmail.Builder("namespace2", "id3")
@@ -2933,7 +3173,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
AppSearchEmail inEmail4 =
new AppSearchEmail.Builder("namespace2", "id4")
@@ -2942,7 +3182,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
// Query with per package result grouping. Only the last document 'email4' should be
@@ -2979,7 +3219,7 @@
@Test
public void testIndexNestedDocuments() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
.addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
@@ -3008,7 +3248,7 @@
new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
.setPropertyDocument("prop", email)
.build();
- checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(yesNestedIndex, noNestedIndex).build()));
// Query.
@@ -3017,7 +3257,7 @@
.setSnippetCount(10)
.setSnippetCountPerProperty(10)
.build());
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
@@ -3030,7 +3270,7 @@
@Test
public void testCJKTQuery() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document to instance 1.
@@ -3038,7 +3278,7 @@
new AppSearchEmail.Builder("namespace", "uri1")
.setBody("ä»–æ˜¯å€‹ç”·å© is a boy")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
// Query for "ä»–" (He)
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index 8d45573..51745dd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -18,6 +18,7 @@
import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
import static com.google.common.truth.Truth.assertThat;
@@ -30,6 +31,7 @@
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.SearchResult;
@@ -53,33 +55,51 @@
public class AppSearchSessionLocalCtsTest extends AppSearchSessionCtsTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, dbName).build());
}
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName, @NonNull ExecutorService executor) {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, dbName)
.setWorkerExecutor(executor).build());
}
+ @Test
+ public void testFeaturesSupported() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
+ new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
+
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)).isTrue();
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)).isTrue();
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)).isTrue();
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)).isTrue();
+ }
+
// TODO(b/194207451) This test can be moved to CtsTestBase if customized logger is
// supported for platform backend.
@Test
public void testLogger_searchStatsLogged_forEmptyFirstPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -97,7 +117,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
assertThat(logger.mSearchStats).isNull();
@@ -111,7 +131,7 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(0);
// Check searchStats has been set. We won't check all the fields here.
@@ -133,12 +153,12 @@
public void testLogger_searchStatsLogged_forNonEmptyFirstPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -156,7 +176,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
assertThat(logger.mSearchStats).isNull();
@@ -170,7 +190,7 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(2);
// Check searchStats has been set. We won't check all the fields here.
@@ -192,12 +212,12 @@
public void testLogger_searchStatsLogged_forEmptySecondPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -215,7 +235,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
// Query for the document
@@ -228,12 +248,12 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(2);
// Get second(empty) page
logger.mSearchStats = null;
- page = searchResults.getNextPage().get();
+ page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(0);
// Check searchStats has been set. We won't check all the fields here.
@@ -255,12 +275,12 @@
public void testLogger_searchStatsLogged_forNonEmptySecondPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -278,7 +298,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
// Query for the document
@@ -291,12 +311,12 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
// Get second page
logger.mSearchStats = null;
- page = searchResults.getNextPage().get();
+ page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
// Check searchStats has been set. We won't check all the fields here.
@@ -318,7 +338,7 @@
public void testSetSchemaStats_withoutSchemaMigration() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
AppSearchSchema appSearchSchema = new AppSearchSchema.Builder("testSchema")
@@ -330,7 +350,7 @@
.build())
.build();
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(appSearchSchema).build()).get();
assertThat(logger.mSetSchemaStats).isNotNull();
@@ -352,7 +372,7 @@
public void testSetSchemaStats_withSchemaMigration() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
@@ -407,11 +427,11 @@
}
};
- db2.setSchema(new SetSchemaRequest.Builder().addSchemas(
+ db2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(
schema).setForceOverride(true).build()).get();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
- db2.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+ db2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(2) // upgrade version
.build()).get();
@@ -424,16 +444,15 @@
assertThat(schemaMigrationStats.getSavedDocumentCount()).isEqualTo(1);
}
- // TODO(b/185441119) Following test can be moved to CtsTestBase if we fix the binder
- // transaction limit in framework.
+ // Framework has max Document size which is 512KiB, this test should only exists in Jetpack.
@Test
- public void testPutLargeDocument() throws Exception {
+ public void testPutLargeDocumentToIcing() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
char[] chars = new char[16_000_000];
@@ -447,7 +466,7 @@
.setSubject("testPut example")
.setBody(body)
.build();
- AppSearchBatchResult<String, Void> result = db2.put(
+ AppSearchBatchResult<String, Void> result = db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
assertThat(result.isSuccess()).isTrue();
@@ -460,16 +479,15 @@
assertThat(outEmail).isEqualTo(email);
}
- // TODO(b/185441119) Following test can be moved to CtsTestBase if we fix the binder
- // transaction limit in framework.
+ // Framework has max Document size which is 512KiB, this test should only exists in Jetpack.
@Test
- public void testPutLargeDocument_exceedLimit() throws Exception {
+ public void testPutLargeDocumentToIcing_exceedLimit() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Create a String property that make the document exceed the total size limit.
@@ -483,7 +501,7 @@
.setSubject("testPut example")
.setBody(body)
.build();
- AppSearchBatchResult<String, Void> result = db2.put(
+ AppSearchBatchResult<String, Void> result = db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
assertThat(result.getFailures()).containsKey("id1");
assertThat(result.getFailures().get("id1").getErrorMessage())
@@ -491,12 +509,42 @@
}
@Test
- public void testCapabilities() throws Exception {
+ public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
- new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
+ AppSearchSession db = LocalStorage.createSearchSessionAsync(
+ new LocalStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document", AppSearchEmail.SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build();
+ db.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
- assertThat(db2.getFeatures().isFeatureSupported(
- Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
+ // Index a document
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+ .setPropertyBytes("bytes")
+ .setPropertyDocument("document")
+ .build();
+
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(db.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+ assertThat(result.getSuccesses()).containsExactly("id1", null);
+ assertThat(result.getFailures()).isEmpty();
+
+ GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id1")
+ .build();
+ List<GenericDocument> outDocuments = doGet(db, request);
+ assertThat(outDocuments).hasSize(1);
+ GenericDocument outDocument = outDocuments.get(0);
+ assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
+ assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
index a0cd21a..57681de 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
@@ -16,15 +16,28 @@
// @exportToFramework:skipFile()
package androidx.appsearch.cts.app;
+import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
+
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.core.os.BuildCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SdkSuppress;
@@ -32,35 +45,102 @@
import org.junit.Test;
+import java.util.List;
import java.util.concurrent.ExecutorService;
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AppSearchSessionPlatformCtsTest extends AppSearchSessionCtsTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createSearchSession(
+ return PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, dbName).build());
}
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName, @NonNull ExecutorService executor) {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createSearchSession(
+ return PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, dbName)
.setWorkerExecutor(executor).build());
}
@Test
- public void testCapabilities() throws Exception {
+ public void testFeaturesSupported() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = PlatformStorage.createSearchSession(
+ AppSearchSession db2 = PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
-
- // TODO(b/201316758) Update to reflect support in Android T+ once this feature is synced
- // over into service-appsearch.
assertThat(db2.getFeatures().isFeatureSupported(
- Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isFalse();
+ Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH))
+ .isEqualTo(BuildCompat.isAtLeastT());
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK))
+ .isEqualTo(BuildCompat.isAtLeastT());
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA))
+ .isEqualTo(BuildCompat.isAtLeastT());
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_GET_BY_ID))
+ .isEqualTo(BuildCompat.isAtLeastT());
+ assertThat(db2.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY))
+ .isEqualTo(BuildCompat.isAtLeastT());
+ }
+
+ @Test
+ public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ AppSearchSession db = PlatformStorage.createSearchSessionAsync(
+ new PlatformStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document", AppSearchEmail.SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build();
+ db.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
+
+ // Index a document
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+ .setPropertyBytes("bytes")
+ .setPropertyDocument("document")
+ .build();
+
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(db.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+ assertThat(result.getSuccesses()).containsExactly("id1", null);
+ assertThat(result.getFailures()).isEmpty();
+
+ GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id1")
+ .build();
+ List<GenericDocument> outDocuments = doGet(db, request);
+ assertThat(outDocuments).hasSize(1);
+ GenericDocument outDocument = outDocuments.get(0);
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+ || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) {
+ // We fixed b/204677124 in Android T, so in S and S_V2, getByteArray and
+ // getDocumentArray will return null if we set empty properties.
+ assertThat(outDocument.getPropertyBytesArray("bytes")).isNull();
+ assertThat(outDocument.getPropertyDocumentArray("document")).isNull();
+ } else {
+ assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
+ assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
+ }
+ }
+
+ @Override
+ @Test
+ public void testPutLargeDocumentBatch() throws Exception {
+ // b/185441119 was fixed in Android T, this test will fail on S_V2 devices and below.
+ assumeTrue(BuildCompat.isAtLeastT());
+ super.testPutLargeDocumentBatch();
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
index 1220731..72faaef 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
@@ -20,12 +20,24 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+
+import com.google.common.collect.ImmutableSet;
import org.junit.Test;
+import java.util.Arrays;
+
public class GetSchemaResponseCtsTest {
@Test
public void testRebuild() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -44,16 +56,111 @@
).build();
GetSchemaResponse.Builder builder =
- new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1);
+ new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1)
+ .addSchemaTypeNotDisplayedBySystem("Email1")
+ .setSchemaTypeVisibleToPackages("Email1",
+ ImmutableSet.of(packageIdentifier1))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ );
GetSchemaResponse original = builder.build();
- GetSchemaResponse rebuild = builder.setVersion(37).addSchema(schema2).build();
+ GetSchemaResponse rebuild = builder.setVersion(37).addSchema(schema2)
+ .addSchemaTypeNotDisplayedBySystem("Email2")
+ .setSchemaTypeVisibleToPackages("Email2",
+ ImmutableSet.of(packageIdentifier2))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA))
+ ).build();
// rebuild won't effect the original object
assertThat(original.getVersion()).isEqualTo(42);
assertThat(original.getSchemas()).containsExactly(schema1);
+ assertThat(original.getSchemaTypesNotDisplayedBySystem())
+ .containsExactly("Email1");
+ assertThat(original.getSchemaTypesVisibleToPackages()).hasSize(1);
+ assertThat(original.getSchemaTypesVisibleToPackages().get("Email1"))
+ .containsExactly(packageIdentifier1);
+ assertThat(original.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
assertThat(rebuild.getVersion()).isEqualTo(37);
assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+ assertThat(rebuild.getSchemaTypesNotDisplayedBySystem())
+ .containsExactly("Email1", "Email2");
+ assertThat(rebuild.getSchemaTypesVisibleToPackages()).hasSize(2);
+ assertThat(rebuild.getSchemaTypesVisibleToPackages().get("Email1"))
+ .containsExactly(packageIdentifier1);
+ assertThat(rebuild.getSchemaTypesVisibleToPackages().get("Email2"))
+ .containsExactly(packageIdentifier2);
+ assertThat(rebuild.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)));
+ }
+
+ @Test
+ public void setVisibility() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+ GetSchemaResponse getSchemaResponse =
+ new GetSchemaResponse.Builder().setVersion(42)
+ .addSchemaTypeNotDisplayedBySystem("Email")
+ .addSchemaTypeNotDisplayedBySystem("Text")
+ .setSchemaTypeVisibleToPackages("Email",
+ ImmutableSet.of(packageIdentifier1, packageIdentifier2))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)))
+ .build();
+
+ assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+ .containsExactly("Email", "Text");
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()).hasSize(1);
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages().get("Email"))
+ .containsExactly(packageIdentifier1, packageIdentifier2);
+ assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility().get("Email"))
+ .containsExactlyElementsIn(ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)));
+ }
+
+ @Test
+ public void getEmptyVisibility() {
+ GetSchemaResponse getSchemaResponse =
+ new GetSchemaResponse.Builder().setVersion(42)
+ .build();
+ assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()).isEmpty();
+ assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
index 1e03d4e..e9458da 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -28,12 +28,15 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
@@ -45,11 +48,13 @@
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.observer.DocumentChangeInfo;
import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
import androidx.appsearch.testutil.AppSearchEmail;
import androidx.appsearch.testutil.TestObserverCallback;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
@@ -75,21 +80,21 @@
protected GlobalSearchSession mGlobalSearchSession;
- protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+ protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@NonNull String dbName);
- protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSession();
+ protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync();
@Before
public void setUp() throws Exception {
- mDb1 = createSearchSession(DB_NAME_1).get();
- mDb2 = createSearchSession(DB_NAME_2).get();
+ mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+ mDb2 = createSearchSessionAsync(DB_NAME_2).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
cleanup();
- mGlobalSearchSession = createGlobalSearchSession().get();
+ mGlobalSearchSession = createGlobalSearchSessionAsync().get();
}
@After
@@ -99,9 +104,9 @@
}
private void cleanup() throws Exception {
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
@@ -128,6 +133,48 @@
}
@Test
+ public void testGlobalGetById() throws Exception {
+ assumeTrue(mGlobalSearchSession.getFeatures().isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
+ SearchSpec exactSearchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+ AppSearchBatchResult<String, GenericDocument> nonExistent =
+ mGlobalSearchSession.getByDocumentIdAsync(mContext.getPackageName(), DB_NAME_1,
+ new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+ .build()).get();
+
+ assertThat(nonExistent.isSuccess()).isFalse();
+ assertThat(nonExistent.getSuccesses()).isEmpty();
+ assertThat(nonExistent.getFailures()).containsKey("id1");
+ assertThat(nonExistent.getFailures().get("id1").getResultCode())
+ .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+ // Index a document
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+ // Query for the document
+ AppSearchBatchResult<String, GenericDocument> afterPutDocuments =
+ mGlobalSearchSession.getByDocumentIdAsync(mContext.getPackageName(), DB_NAME_1,
+ new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+ .build()).get();
+ assertThat(afterPutDocuments.getSuccesses()).containsExactly("id1", inEmail);
+ }
+
+ @Test
public void testGlobalQuery_oneInstance() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec = new SearchSpec.Builder()
@@ -138,7 +185,7 @@
exactSearchSpec);
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document
@@ -149,7 +196,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
@@ -173,9 +220,9 @@
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a document to instance 1.
@@ -186,7 +233,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
// Index a document to instance 2.
@@ -197,7 +244,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
// Query across all instances
@@ -215,7 +262,7 @@
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
List<AppSearchEmail> emailList = new ArrayList<>();
PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
@@ -232,7 +279,7 @@
emailList.add(inEmail);
putDocumentsRequestBuilder.addGenericDocuments(inEmail);
}
- checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+ checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
// Set number of results per page is 7.
int pageSize = 7;
@@ -248,7 +295,7 @@
// keep loading next page until it's empty.
do {
- results = searchResults.getNextPage().get();
+ results = searchResults.getNextPageAsync().get();
++pageNumber;
for (SearchResult result : results) {
documents.add(result.getGenericDocument());
@@ -293,19 +340,19 @@
).build();
// db1 has both "Generic" and "builtin:Email"
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(genericSchema).addSchemas(AppSearchEmail.SCHEMA).build()).get();
// db2 only has "builtin:Email"
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index a generic document into db1
GenericDocument genericDocument = new GenericDocument.Builder<>("namespace", "id2",
"Generic")
.setPropertyString("foo", "body").build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(genericDocument).build()));
@@ -318,9 +365,9 @@
.build();
// Put the email in both databases
- checkIsBatchResultSuccess((mDb1.put(
+ checkIsBatchResultSuccess((mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
// Query for all documents across types
@@ -352,9 +399,9 @@
exactNamespace1SearchSpec);
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index two documents
@@ -365,7 +412,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(document1).build()));
@@ -376,7 +423,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
// Query for all namespaces
@@ -410,9 +457,9 @@
testPackageSearchSpec);
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index two documents
@@ -423,7 +470,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(document1).build()));
@@ -434,7 +481,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
// Query in some other package
@@ -454,11 +501,11 @@
@Test
public void testGlobalQuery_projectionTwoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -472,7 +519,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1).build()));
@@ -484,7 +531,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email2).build()));
@@ -516,11 +563,11 @@
@Test
public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -534,7 +581,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1).build()));
@@ -546,7 +593,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email2).build()));
@@ -573,11 +620,11 @@
@Test
public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
// Schema registration
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
- mDb2.setSchema(
+ mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.build()).get();
@@ -591,7 +638,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1).build()));
@@ -603,7 +650,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email2).build()));
@@ -636,9 +683,9 @@
@Test
public void testQuery_ResultGroupingLimits() throws Exception {
// Schema registration
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index one document in 'namespace1' and one document in 'namespace2' into db1.
@@ -649,7 +696,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
AppSearchEmail inEmail2 =
new AppSearchEmail.Builder("namespace2", "id2")
@@ -658,7 +705,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb1.put(
+ checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
// Index one document in 'namespace1' and one document in 'namespace2' into db2.
@@ -669,7 +716,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
AppSearchEmail inEmail4 =
new AppSearchEmail.Builder("namespace2", "id4")
@@ -678,7 +725,7 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(mDb2.put(
+ checkIsBatchResultSuccess(mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
// Query with per package result grouping. Only the last document 'email4' should be
@@ -716,7 +763,7 @@
@Test
public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
// Index a document
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
AppSearchEmail email1 =
new AppSearchEmail.Builder("namespace", "id1")
@@ -727,7 +774,8 @@
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1).build()));
// Query
List<SearchResult> page;
@@ -735,13 +783,13 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build())) {
- page = results.getNextPage().get();
+ page = results.getNextPageAsync().get();
}
assertThat(page).isNotEmpty();
SearchResult firstResult = page.get(0);
ExecutionException exception = assertThrows(
- ExecutionException.class, () -> mGlobalSearchSession.reportSystemUsage(
+ ExecutionException.class, () -> mGlobalSearchSession.reportSystemUsageAsync(
new ReportSystemUsageRequest.Builder(
firstResult.getPackageName(),
firstResult.getDatabaseName(),
@@ -758,56 +806,63 @@
@Test
public void testAddObserver_notSupported() {
assumeFalse(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
assertThrows(
UnsupportedOperationException.class,
- () -> mGlobalSearchSession.addObserver(
+ () -> mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
new TestObserverCallback()));
assertThrows(
UnsupportedOperationException.class,
- () -> mGlobalSearchSession.removeObserver(
+ () -> mGlobalSearchSession.unregisterObserverCallback(
mContext.getPackageName(), new TestObserverCallback()));
}
@Test
public void testAddObserver() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback observer = new TestObserverCallback();
// Register observer. Note: the type does NOT exist yet!
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
observer);
// Index a document
- mDb1.setSchema(
+ mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1).build()));
// Make sure the notification was received.
- observer.waitForNotificationCount(1);
- assertThat(observer.getSchemaChanges()).isEmpty();
+ observer.waitForNotificationCount(2);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ DB_NAME_1,
+ /*changedSchemaNames=*/ImmutableSet.of(AppSearchEmail.SCHEMA_TYPE)));
assertThat(observer.getDocumentChanges()).containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE));
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1"))
+ );
}
@Test
public void testRegisterObserver_MultiType() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
@@ -816,18 +871,18 @@
AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Register two observers. One has no filters, the other filters on email.
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
@@ -845,14 +900,17 @@
"namespace2", "id2", "Gift").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1).build()));
checkIsBatchResultSuccess(
- mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+ mDb2.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(gift1).build()));
// Make sure the notification was received.
unfilteredObserver.waitForNotificationCount(5);
@@ -864,27 +922,32 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
- "Gift"),
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
- "Gift")
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id2"))
);
// Check the filtered observer
@@ -894,24 +957,27 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
- AppSearchEmail.SCHEMA_TYPE)
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1"))
);
}
@Test
public void testRegisterObserver_removeById() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
@@ -920,14 +986,14 @@
AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
// Register two observers. One, registered later, has no filters. The other, registered
// now, filters on email.
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
@@ -945,27 +1011,29 @@
"namespace2", "id2", "Gift").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1).build()));
checkIsBatchResultSuccess(
- mDb2.put(new PutDocumentsRequest.Builder()
+ mDb2.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1).build()));
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(gift1).build()));
// Register the second observer
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
// Remove some of the documents.
- checkIsBatchResultSuccess(mDb1.remove(
+ checkIsBatchResultSuccess(mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
- checkIsBatchResultSuccess(mDb2.remove(
+ checkIsBatchResultSuccess(mDb2.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace2").addIds("id2").build()));
// Make sure the notification was received. emailObserver should have seen:
@@ -981,22 +1049,26 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE)
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1"))
);
// Check unfilteredObserver
@@ -1006,19 +1078,21 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace2",
- "Gift")
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id2"))
);
}
@Test
public void testRegisterObserver_removeByQuery() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
@@ -1027,9 +1101,9 @@
AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
// Index some documents
@@ -1040,19 +1114,19 @@
"namespace2", "id3", "Gift").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2, gift1).build()));
checkIsBatchResultSuccess(
- mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(
+ mDb2.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(
email1, email2, gift1).build()));
// Register observers
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
@@ -1065,20 +1139,16 @@
assertThat(emailObserver.getDocumentChanges()).isEmpty();
// Remove "cat" emails in db1 and all types in db2
- mDb1.remove(
- "cat",
- new SearchSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build())
+ mDb1.removeAsync("cat",
+ new SearchSpec.Builder()
+ .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build())
.get();
- mDb2.remove("", new SearchSpec.Builder().build()).get();
+ mDb2.removeAsync("", new SearchSpec.Builder().build()).get();
// Make sure the notification was received. UnfilteredObserver should have seen:
// -db1:id2, -db2:id1, -db2:id2, -db2:id3
// emailObserver should have seen:
// -db1:id2, -db2:id1, -db2:id2
- // TODO(b/193494000): Notifications are currently grouped by
- // (package, database, namespace, schema). This causes -db2:id1 and -db2:id2 to be combined
- // into one notification. Once notifications have IDs, we need to check to make sure all
- // the individual IDs are reported in the combined notification.
unfilteredObserver.waitForNotificationCount(3);
emailObserver.waitForNotificationCount(2);
@@ -1088,17 +1158,20 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1", "id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace2",
- "Gift")
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id3"))
);
// Check emailObserver
@@ -1108,19 +1181,21 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
- AppSearchEmail.SCHEMA_TYPE)
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1", "id2"))
);
}
@Test
public void testRegisterObserver_sameCallback_differentSpecs() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback observer = new TestObserverCallback();
@@ -1128,16 +1203,16 @@
AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
// Register the same observer twice: once for gift, once for email
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
EXECUTOR,
observer);
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
@@ -1149,7 +1224,7 @@
"namespace2", "id3", "Gift").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1).build()));
// Make sure the same observer received both values
@@ -1160,19 +1235,21 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
- "Gift")
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id3"))
);
}
@Test
public void testRemoveObserver() throws Exception {
assumeTrue(mGlobalSearchSession.getFeatures()
- .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback temporaryObserver = new TestObserverCallback();
TestObserverCallback permanentObserver = new TestObserverCallback();
@@ -1181,24 +1258,24 @@
AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
- mDb1.setSchema(new SetSchemaRequest.Builder()
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
- mDb2.setSchema(new SetSchemaRequest.Builder()
+ mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
// Register both observers. temporaryObserver is registered twice to ensure both instances
// get removed.
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
temporaryObserver);
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
EXECUTOR,
temporaryObserver);
- mGlobalSearchSession.addObserver(
+ mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
@@ -1220,7 +1297,7 @@
"namespace3", "id4", "Gift").build();
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1).build()));
// Make sure the notifications were received.
@@ -1232,12 +1309,14 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
- "Gift"));
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id3")));
assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
assertThat(temporaryObserver.getDocumentChanges())
.containsExactlyElementsIn(expectedChangesOrig);
@@ -1246,11 +1325,12 @@
.containsExactlyElementsIn(expectedChangesOrig);
// Unregister temporaryObserver
- mGlobalSearchSession.removeObserver(mContext.getPackageName(), temporaryObserver);
+ mGlobalSearchSession.unregisterObserverCallback(
+ mContext.getPackageName(), temporaryObserver);
// Index some more documents
checkIsBatchResultSuccess(
- mDb1.put(new PutDocumentsRequest.Builder()
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
.addGenericDocuments(email2, gift2).build()));
// Only the permanent observer should have received this
@@ -1263,25 +1343,300 @@
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
- "Gift"),
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id3")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
- AppSearchEmail.SCHEMA_TYPE),
+ AppSearchEmail.SCHEMA_TYPE,
+ /*changedDocumentIds=*/ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace3",
- "Gift")
+ "Gift",
+ /*changedDocumentIds=*/ImmutableSet.of("id4"))
);
assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
assertThat(temporaryObserver.getDocumentChanges())
.containsExactlyElementsIn(expectedChangesOrig);
}
+
+ @Test
+ public void testGlobalGetSchema() throws Exception {
+ assumeTrue(mGlobalSearchSession.getFeatures()
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
+
+ // One schema should be set with global access and the other should be set with local
+ // access.
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+ mDb2.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(
+ AppSearchEmail.SCHEMA).setSchemaTypeDisplayedBySystem(
+ AppSearchEmail.SCHEMA_TYPE, /*displayed=*/false).build()).get();
+
+ GetSchemaResponse response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(),
+ DB_NAME_1).get();
+ assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+
+ response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_2).get();
+ assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+
+ // A request for a db that doesn't exist should return a response with no schemas.
+ response = mGlobalSearchSession.getSchemaAsync(
+ mContext.getPackageName(), "NonexistentDb").get();
+ assertThat(response.getSchemas()).isEmpty();
+ }
+
+ @Test
+ public void testGlobalGetSchema_notSupported() throws Exception {
+ assumeFalse(mGlobalSearchSession.getFeatures()
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
+
+ // One schema should be set with global access and the other should be set with local
+ // access.
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+ UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class,
+ () -> mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_1));
+ assertThat(e).hasMessageThat().isEqualTo(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
+ + " is not supported on this AppSearch implementation.");
+ }
+
+ @Test
+ public void testGlobalGetByDocumentId_notSupported() throws Exception {
+ assumeFalse(mGlobalSearchSession.getFeatures()
+ .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
+
+ Context context = ApplicationProvider.getApplicationContext();
+
+ UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class,
+ () -> mGlobalSearchSession.getByDocumentIdAsync(context.getPackageName(), DB_NAME_1,
+ new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id").build()));
+
+ assertThat(e).hasMessageThat().isEqualTo(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
+ + " is not supported on this AppSearch implementation.");
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_added() throws Exception {
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mGlobalSearchSession.registerObserverCallback(
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ EXECUTOR,
+ observer);
+
+ // Add a schema type
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("Type1").build())
+ .build())
+ .get();
+
+ observer.waitForNotificationCount(1);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ DB_NAME_1,
+ ImmutableSet.of("Type1")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Add two more schema types without touching the existing one
+ observer.clear();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build(),
+ new AppSearchSchema.Builder("Type3").build())
+ .build())
+ .get();
+
+ observer.waitForNotificationCount(1);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2", "Type3")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_removed() throws Exception {
+ // Add a schema type
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build())
+ .build())
+ .get();
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mGlobalSearchSession.registerObserverCallback(
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ EXECUTOR,
+ observer);
+
+ // Remove Type2
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("Type1").build())
+ .setForceOverride(true)
+ .build())
+ .get();
+
+ observer.waitForNotificationCount(1);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents() throws Exception {
+ // Add a schema
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build())
+ .build())
+ .get();
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mGlobalSearchSession.registerObserverCallback(
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ EXECUTOR,
+ observer);
+
+ // Update the schema, but don't make any actual changes
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build())
+ .build())
+ .get();
+
+ // Now update the schema again, but this time actually make a change (cardinality of the
+ // property)
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build())
+ .build())
+ .get();
+
+ // Dispatch notifications
+ observer.waitForNotificationCount(1);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+ // Add a schema
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build())
+ .build())
+ .get();
+
+ // Register an observer that only listens for Type2
+ TestObserverCallback observer = new TestObserverCallback();
+ mGlobalSearchSession.registerObserverCallback(
+ /*targetPackageName=*/mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
+ EXECUTOR,
+ observer);
+
+ // Update both types of the schema (changed cardinalities)
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build())
+ .build())
+ .get();
+
+ observer.waitForNotificationCount(1);
+ assertThat(observer.getSchemaChanges()).containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ // TODO(b/193494000): Properly handle change notification during schema migration, and add tests
+ // for it.
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
index 19af478..a406841 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
@@ -23,9 +23,11 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.SearchResult;
@@ -46,25 +48,29 @@
public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createSearchSession(
+ return LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, dbName).build());
}
@Override
- protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+ protected ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync() {
Context context = ApplicationProvider.getApplicationContext();
- return LocalStorage.createGlobalSearchSession(
+ return LocalStorage.createGlobalSearchSessionAsync(
new LocalStorage.GlobalSearchContext.Builder(context).build());
}
@Test
- public void testFeaturesSupported() {
- assertThat(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
- assertThat(mDb1.getFeatures().isFeatureSupported(
- Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)).isTrue();
+ public void testGlobalGetById_jetpackMultiPackage() throws Exception {
+ // Can't global get document in different package in jetpack
+ AppSearchBatchResult<String, GenericDocument> fakePackage =
+ mGlobalSearchSession.getByDocumentIdAsync("fake", DB_NAME_1,
+ new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+ .build()).get();
+ assertThat(fakePackage.getFailures()).hasSize(1);
+ assertThat(fakePackage.getFailures().get("id1").getResultCode())
+ .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
}
// TODO(b/194207451) This test can be moved to CtsTestBase if customized logger is
@@ -73,12 +79,12 @@
public void testLogger_searchStatsLogged_forEmptyFirstPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -96,10 +102,10 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
- GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+ GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
logger).build()).get();
assertThat(logger.mSearchStats).isNull();
@@ -113,7 +119,7 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(0);
// Check searchStats has been set. We won't check all the fields here.
@@ -134,12 +140,12 @@
public void testLogger_searchStatsLogged_forNonEmptyFirstPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -157,10 +163,10 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
- GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+ GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
logger).build()).get();
assertThat(logger.mSearchStats).isNull();
@@ -174,7 +180,7 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
// Check searchStats has been set. We won't check all the fields here.
@@ -195,12 +201,12 @@
public void testLogger_searchStatsLogged_forEmptySecondPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -218,10 +224,10 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
- GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+ GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
logger).build()).get();
assertThat(logger.mSearchStats).isNull();
@@ -235,12 +241,12 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(2);
// Get second(empty) page
logger.mSearchStats = null;
- page = searchResults.getNextPage().get();
+ page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(0);
// Check searchStats has been set. We won't check all the fields here.
@@ -261,12 +267,12 @@
public void testLogger_searchStatsLogged_forNonEmptySecondPage() throws Exception {
SimpleTestLogger logger = new SimpleTestLogger();
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession db2 = LocalStorage.createSearchSession(
+ AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
.setLogger(logger).build()).get();
// Schema registration
- db2.setSchema(
+ db2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
// Index documents
@@ -284,10 +290,10 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- checkIsBatchResultSuccess(db2.put(
+ checkIsBatchResultSuccess(db2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
- GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+ GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
logger).build()).get();
assertThat(logger.mSearchStats).isNull();
@@ -301,12 +307,12 @@
.build());
// Get first page
- List<SearchResult> page = searchResults.getNextPage().get();
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
// Get second page
logger.mSearchStats = null;
- page = searchResults.getNextPage().get();
+ page = searchResults.getNextPageAsync().get();
assertThat(page).hasSize(1);
// Check searchStats has been set. We won't check all the fields here.
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
index 9d152ff..46aa5c4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
@@ -16,14 +16,11 @@
// @exportToFramework:skipFile()
package androidx.appsearch.cts.app;
-import static com.google.common.truth.Truth.assertThat;
-
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Features;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.platformstorage.PlatformStorage;
import androidx.test.core.app.ApplicationProvider;
@@ -31,33 +28,19 @@
import com.google.common.util.concurrent.ListenableFuture;
-import org.junit.Ignore;
-import org.junit.Test;
-
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class GlobalSearchSessionPlatformCtsTest extends GlobalSearchSessionCtsTestBase {
@Override
- protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+ protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createSearchSession(
+ return PlatformStorage.createSearchSessionAsync(
new PlatformStorage.SearchContext.Builder(context, dbName).build());
}
@Override
- protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+ protected ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync() {
Context context = ApplicationProvider.getApplicationContext();
- return PlatformStorage.createGlobalSearchSession(
+ return PlatformStorage.createGlobalSearchSessionAsync(
new PlatformStorage.GlobalSearchContext.Builder(context).build());
}
-
- @Ignore("Unignore this test once these features are implemented in the platform backend.")
- @Test
- public void testFeaturesSupported() {
- assertThat(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH))
- .isEqualTo(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
- assertThat(mDb1.getFeatures().isFeatureSupported(
- Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER))
- .isEqualTo(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
- }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
index 372a090..c2b06d4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
@@ -74,12 +74,12 @@
// A schema with Card must be set in order to be able to add a Card instance to
// PutDocumentsRequest.
Context context = ApplicationProvider.getApplicationContext();
- AppSearchSession session = LocalStorage.createSearchSession(
+ AppSearchSession session = LocalStorage.createSearchSessionAsync(
new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "")
.build()
).get();
- session.setSchema(new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build())
- .get();
+ session.setSchemaAsync(
+ new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build()).get();
Set<Card> cards = ImmutableSet.of(new Card("cardNamespace", "cardId", "cardProperty"));
PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(cards)
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
index a965ef6..dd47914 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -220,6 +220,87 @@
}
@Test
+ public void testSetSchemaTypeVisibleForPermissions() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ // By default, the schema is displayed.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchemas(schema).build();
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
+
+ SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+ .addSchemas(schema)
+ .addRequiredPermissionsForSchemaTypeVisibility("Schema",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForSchemaTypeVisibility("Schema",
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+
+ request = setSchemaRequestBuilder.build();
+
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly("Schema", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_HOME_APP_SEARCH_DATA)));
+ }
+
+ @Test
+ public void testClearSchemaTypeVisibleForPermissions() {
+ SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+ .addSchemas(
+ new AppSearchSchema.Builder("Schema1").build(),
+ new AppSearchSchema.Builder("Schema2").build())
+ .addRequiredPermissionsForSchemaTypeVisibility(
+ "Schema1",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForSchemaTypeVisibility(
+ "Schema1",
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .addRequiredPermissionsForSchemaTypeVisibility(
+ "Schema2",
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE));
+
+ SetSchemaRequest request = setSchemaRequestBuilder.build();
+
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "Schema1", ImmutableSet.of(
+ ImmutableSet.of(
+ SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Schema2", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+
+ // Clear the permissions in the builder
+ setSchemaRequestBuilder.clearRequiredPermissionsForSchemaTypeVisibility("Schema1");
+
+ // New object should be updated
+ assertThat(setSchemaRequestBuilder.build().getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "Schema2", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+
+ // Old object should remain unchanged
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "Schema1", ImmutableSet.of(
+ ImmutableSet.of(
+ SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Schema2", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+ }
+
+ @Test
public void testSchemaTypeVisibilityForPackage_visible() {
AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
@@ -380,6 +461,29 @@
}
@Test
+ public void testSetDocumentClassVisibleForPermission() throws Exception {
+ // By default, the schema is displayed.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
+
+ SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+ .addDocumentClasses(Card.class)
+ .addRequiredPermissionsForDocumentClassVisibility(Card.class,
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForDocumentClassVisibility(Card.class,
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+ request = setSchemaRequestBuilder.build();
+
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly("Card", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_HOME_APP_SEARCH_DATA)));
+ }
+
+ @Test
public void testSetDocumentClassVisibilityForPackage_visible() throws Exception {
// By default, the schema is not visible.
SetSchemaRequest request =
@@ -478,6 +582,58 @@
"King");
}
+ @Test
+ public void testClearDocumentClassVisibleForPermissions() throws Exception {
+ SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+ .addDocumentClasses(King.class, Queen.class)
+ .addRequiredPermissionsForDocumentClassVisibility(
+ King.class,
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForDocumentClassVisibility(
+ King.class,
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .addRequiredPermissionsForDocumentClassVisibility(
+ Queen.class,
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE));
+
+ SetSchemaRequest request = setSchemaRequestBuilder.build();
+
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "King", ImmutableSet.of(
+ ImmutableSet.of(
+ SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Queen", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+
+ // Clear the permissions in the builder
+ setSchemaRequestBuilder.clearRequiredPermissionsForDocumentClassVisibility(King.class);
+
+ // New object should be updated
+ assertThat(setSchemaRequestBuilder.build().getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "Queen", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+
+ // Old object should remain unchanged
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ "King", ImmutableSet.of(
+ ImmutableSet.of(
+ SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Queen", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+ )
+ );
+ }
// @exportToFramework:endStrip()
@Test
@@ -498,4 +654,130 @@
assertThat(exception).hasMessageThat().contains(
"Cannot set version to the request if schema is empty.");
}
+
+ @Test
+ public void testRebuild() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+ .addSchemas(schema1)
+ .setVersion(37)
+ .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+ .setSchemaTypeVisibilityForPackage(
+ "Email1", /*visible=*/true, packageIdentifier1)
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+
+ SetSchemaRequest original = builder.build();
+ SetSchemaRequest rebuild = builder.addSchemas(schema2)
+ .setVersion(42)
+ .setSchemaTypeDisplayedBySystem("Email2", /*displayed=*/false)
+ .setSchemaTypeVisibilityForPackage(
+ "Email2", /*visible=*/true, packageIdentifier2)
+ .addRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE))
+ .addRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))
+ .build();
+
+ assertThat(original.getSchemas()).containsExactly(schema1);
+ assertThat(original.getVersion()).isEqualTo(37);
+ assertThat(original.getSchemasNotDisplayedBySystem()).containsExactly("Email1");
+ assertThat(original.getSchemasVisibleToPackages()).containsExactly(
+ "Email1", ImmutableSet.of(packageIdentifier1));
+ assertThat(original.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+
+ assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+ assertThat(rebuild.getVersion()).isEqualTo(42);
+ assertThat(rebuild.getSchemasNotDisplayedBySystem()).containsExactly("Email1", "Email2");
+ assertThat(rebuild.getSchemasVisibleToPackages()).containsExactly(
+ "Email1", ImmutableSet.of(packageIdentifier1),
+ "Email2", ImmutableSet.of(packageIdentifier2));
+ assertThat(rebuild.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+ "Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+ }
+
+ @Test
+ public void getAndModify() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1)
+ .setVersion(37)
+ .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+ .setSchemaTypeVisibilityForPackage(
+ "Email1", /*visible=*/true, packageIdentifier1)
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+ .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+
+ // get the visibility setting and modify the output object.
+ // skip getSchemasNotDisplayedBySystem since it returns an unmodifiable object.
+ request.getSchemasVisibleToPackages().put("Email2", ImmutableSet.of(packageIdentifier2));
+ request.getRequiredPermissionsForSchemaTypeVisibility().put("Email2",
+ ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CALENDAR)));
+
+ // verify we still get the original object.
+ assertThat(request.getSchemasVisibleToPackages()).containsExactly("Email1",
+ ImmutableSet.of(packageIdentifier1));
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
new file mode 100644
index 0000000..06f1bd4
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.cts.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.observer.DocumentChangeInfo;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class DocumentChangeInfoCtsTest {
+ @Test
+ public void testConstructor() {
+ DocumentChangeInfo DocumentChangeInfo = new DocumentChangeInfo(
+ "packageName",
+ "databaseName",
+ "namespace",
+ "SchemaName",
+ ImmutableSet.of("documentId1", "documentId2"));
+ assertThat(DocumentChangeInfo.getPackageName()).isEqualTo("packageName");
+ assertThat(DocumentChangeInfo.getDatabaseName()).isEqualTo("databaseName");
+ assertThat(DocumentChangeInfo.getNamespace()).isEqualTo("namespace");
+ assertThat(DocumentChangeInfo.getSchemaName()).isEqualTo("SchemaName");
+ assertThat(DocumentChangeInfo.getChangedDocumentIds())
+ .containsExactly("documentId1", "documentId2");
+ }
+
+ @Test
+ public void testEqualsAndHasCode() {
+ DocumentChangeInfo info1Copy1 = new DocumentChangeInfo(
+ "packageName",
+ "databaseName",
+ "namespace",
+ "SchemaName",
+ ImmutableSet.of("documentId1", "documentId2"));
+ DocumentChangeInfo info1Copy2 = new DocumentChangeInfo(
+ "packageName",
+ "databaseName",
+ "namespace",
+ "SchemaName",
+ ImmutableSet.of("documentId1", "documentId2"));
+ DocumentChangeInfo info2 = new DocumentChangeInfo(
+ "packageName",
+ "databaseName",
+ "namespace",
+ "SchemaName",
+ ImmutableSet.of("documentId3", "documentId2"));
+
+ assertThat(info1Copy1).isEqualTo(info1Copy2);
+ assertThat(info1Copy1.hashCode()).isEqualTo(info1Copy2.hashCode());
+ assertThat(info1Copy1).isNotEqualTo(info2);
+ assertThat(info1Copy1.hashCode()).isNotEqualTo(info2.hashCode());
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java
new file mode 100644
index 0000000..ae321af
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.cts.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.observer.ObserverSpec;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class ObserverSpecCtsTest {
+ @Test
+ public void testFilterSchemas() {
+ ObserverSpec observerSpec = new ObserverSpec.Builder()
+ .addFilterSchemas("Schema1", "Schema2")
+ .addFilterSchemas(ImmutableSet.of("Schema3", "Schema4"))
+ .build();
+ assertThat(observerSpec.getFilterSchemas()).containsExactly(
+ "Schema1", "Schema2", "Schema3", "Schema4");
+ }
+
+// @exportToFramework:startStrip()
+
+ @Document
+ public static class King {
+ @Document.Namespace String mNamespace;
+ @Document.Id String mId;
+ }
+
+ @Document
+ public static class Queen {
+ @Document.Namespace String mNamespace;
+ @Document.Id String mId;
+ }
+
+ @Document
+ public static class Jack {
+ @Document.Namespace String mNamespace;
+ @Document.Id String mId;
+ }
+
+ @Document
+ public static class Ace {
+ @Document.Namespace String mNamespace;
+ @Document.Id String mId;
+ }
+
+ @Test
+ public void testFilterSchemas_documentClass() throws Exception {
+ ObserverSpec observerSpec = new ObserverSpec.Builder()
+ .addFilterSchemas("Schema1", "Schema2")
+ .addFilterDocumentClasses(King.class, Queen.class)
+ .addFilterSchemas(ImmutableSet.of("Schema3", "Schema4"))
+ .addFilterDocumentClasses(ImmutableSet.of(Jack.class, Ace.class))
+ .build();
+ assertThat(observerSpec.getFilterSchemas()).containsExactly(
+ "Schema1", "Schema2", "King", "Queen", "Schema3", "Schema4", "Jack", "Ace");
+ }
+
+// @exportToFramework:endStrip()
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java
new file mode 100644
index 0000000..3289177
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.cts.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.observer.SchemaChangeInfo;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class SchemaChangeInfoCtsTest {
+ @Test
+ public void testConstructor() {
+ SchemaChangeInfo schemaChangeInfo = new SchemaChangeInfo(
+ "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+ assertThat(schemaChangeInfo.getPackageName()).isEqualTo("packageName");
+ assertThat(schemaChangeInfo.getDatabaseName()).isEqualTo("databaseName");
+ assertThat(schemaChangeInfo.getChangedSchemaNames())
+ .containsExactly("schemaName1", "schemaName2");
+ }
+
+ @Test
+ public void testEqualsAndHasCode() {
+ SchemaChangeInfo info1Copy1 = new SchemaChangeInfo(
+ "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+ SchemaChangeInfo info1Copy2 = new SchemaChangeInfo(
+ "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+ SchemaChangeInfo info2 = new SchemaChangeInfo(
+ "packageName", "databaseName", ImmutableSet.of("schemaName3", "schemaName2"));
+
+ assertThat(info1Copy1).isEqualTo(info1Copy2);
+ assertThat(info1Copy1.hashCode()).isEqualTo(info1Copy2.hashCode());
+ assertThat(info1Copy1).isNotEqualTo(info2);
+ assertThat(info1Copy1.hashCode()).isNotEqualTo(info2.hashCode());
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
index 527127f..ef685c1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -220,7 +220,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
@@ -256,7 +256,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
@@ -280,7 +280,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
@@ -307,7 +307,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
@@ -331,7 +331,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
@@ -355,7 +355,7 @@
* <p>This attribute does not apply to properties of a repeated type (e.g. a list).
*
* <p>Please make sure you understand the consequences of required fields on
- * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
* this attribute to {@code true}.
*/
boolean required() default false;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
index 4b6ee7c..3cdf6ce 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -37,9 +37,9 @@
* @param <KeyType> The type of the keys for which the results will be reported.
* @param <ValueType> The type of the result objects for successful results.
*
- * @see AppSearchSession#put
- * @see AppSearchSession#getByDocumentId
- * @see AppSearchSession#remove
+ * @see AppSearchSession#putAsync
+ * @see AppSearchSession#getByDocumentIdAsync
+ * @see AppSearchSession#removeAsync
*/
public final class AppSearchBatchResult<KeyType, ValueType> {
@NonNull private final Map<KeyType, ValueType> mSuccesses;
@@ -64,9 +64,9 @@
* Returns a {@link Map} of keys mapped to instances of the value type for all successful
* individual results.
*
- * <p>Example: {@link AppSearchSession#getByDocumentId} returns an {@link AppSearchBatchResult}.
- * Each key (the document ID, of {@code String} type) will map to a {@link GenericDocument}
- * object.
+ * <p>Example: {@link AppSearchSession#getByDocumentIdAsync} returns an
+ * {@link AppSearchBatchResult}. Each key (the document ID, of {@code String} type) will map to
+ * a {@link GenericDocument} object.
*
* <p>The values of the {@link Map} will not be {@code null}.
*/
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 84795f9..c3e8b5e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -44,7 +44,7 @@
*
* <p>The schema consists of type information, properties, and config (like tokenization type).
*
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
*/
public final class AppSearchSchema {
private static final String SCHEMA_TYPE_FIELD = "schemaType";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index d423da0..be67b84f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -32,7 +32,8 @@
* a schema, adding documents, and searching.
*
* <p>Instances of this interface are usually obtained from a storage implementation, e.g.
- * {@code LocalStorage.createSearchSession()} or {@code PlatformStorage.createSearchSession()}.
+ * {@code LocalStorage.createSearchSessionAsync()} or
+ * {@code PlatformStorage.createSearchSessionAsync()}.
*
* <p>All implementations of this interface must be thread safe.
*
@@ -44,42 +45,75 @@
* Sets the schema that represents the organizational structure of data within the AppSearch
* database.
*
- * <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
- * schema needs to be updated, or it has not been previously set, then the provided schema
- * will be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently
- * as a no-op call.
+ * <p>Upon creating an {@link AppSearchSession}, {@link #setSchemaAsync} should be called. If
+ * the schema needs to be updated, or it has not been previously set, then the provided schema
+ * will be saved and persisted to disk. Otherwise, {@link #setSchemaAsync} is handled
+ * efficiently as a no-op call.
*
* @param request the schema to set or update the AppSearch database to.
* @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
*/
@NonNull
- ListenableFuture<SetSchemaResponse> setSchema(
- @NonNull SetSchemaRequest request);
+ ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request);
/**
- * Retrieves the schema most recently successfully provided to {@link #setSchema}.
+ * @deprecated use {@link #setSchemaAsync}
+ * @param request the schema to set or update the AppSearch database to.
+ * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<SetSchemaResponse> setSchema(
+ @NonNull SetSchemaRequest request) {
+ return setSchemaAsync(request);
+ }
+
+ /**
+ * Retrieves the schema most recently successfully provided to {@link #setSchemaAsync}.
*
* @return The pending {@link GetSchemaResponse} of performing this operation.
*/
// This call hits disk; async API prevents us from treating these calls as properties.
@SuppressLint("KotlinPropertyAccess")
@NonNull
- ListenableFuture<GetSchemaResponse> getSchema();
+ ListenableFuture<GetSchemaResponse> getSchemaAsync();
+
+ /**
+ * @deprecated use {@link #getSchemaAsync}
+ *
+ * @return The pending {@link GetSchemaResponse} of performing this operation.
+ */
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ @Deprecated
+ default ListenableFuture<GetSchemaResponse> getSchema() {
+ return getSchemaAsync();
+ }
/**
* Retrieves the set of all namespaces in the current database with at least one document.
*
- * @return The pending result of performing this operation.
- */
+ * @return The pending result of performing this operation. */
@NonNull
- ListenableFuture<Set<String>> getNamespaces();
+ ListenableFuture<Set<String>> getNamespacesAsync();
+
+ /**
+ * @deprecated use {@link #getNamespacesAsync()}
+ *
+ * @return The pending result of performing this operation. */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<Set<String>> getNamespaces() {
+ return getNamespacesAsync();
+ }
/**
* Indexes documents into the {@link AppSearchSession} database.
*
* <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an
* {@link AppSearchSchema} type that has been previously registered by calling the
- * {@link #setSchema} method.
+ * {@link #setSchemaAsync} method.
*
* @param request containing documents to be indexed.
* @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
@@ -88,7 +122,24 @@
* or a failed {@link AppSearchResult} otherwise.
*/
@NonNull
- ListenableFuture<AppSearchBatchResult<String, Void>> put(@NonNull PutDocumentsRequest request);
+ ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
+ @NonNull PutDocumentsRequest request);
+
+ /**
+ * @deprecated use {@link #putAsync}
+ *
+ * @param request containing documents to be indexed.
+ * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+ * The keys of the returned {@link AppSearchBatchResult} are the IDs of the input documents.
+ * The values are either {@code null} if the corresponding document was successfully indexed,
+ * or a failed {@link AppSearchResult} otherwise.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<AppSearchBatchResult<String, Void>> put(
+ @NonNull PutDocumentsRequest request) {
+ return putAsync(request);
+ }
/**
* Gets {@link GenericDocument} objects by document IDs in a namespace from the
@@ -104,10 +155,29 @@
* {@link AppSearchResult#RESULT_NOT_FOUND}.
*/
@NonNull
- ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+ ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
@NonNull GetByDocumentIdRequest request);
/**
+ * @deprecated use {@link #getByDocumentIdAsync}
+ *
+ * @param request a request containing a namespace and IDs to get documents for.
+ * @return A {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+ * The keys of the {@link AppSearchBatchResult} represent the input document IDs from the
+ * {@link GetByDocumentIdRequest} object. The values are either the corresponding
+ * {@link GenericDocument} object for the ID on success, or an {@link AppSearchResult}
+ * object on failure. For example, if an ID is not found, the value for that ID will be set
+ * to an {@link AppSearchResult} object with result code:
+ * {@link AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+ @NonNull GetByDocumentIdRequest request) {
+ return getByDocumentIdAsync(request);
+ }
+
+ /**
* Retrieves documents from the open {@link AppSearchSession} that match a given query string
* and type of search provided.
*
@@ -164,7 +234,7 @@
* adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
*
* <p>This method is lightweight. The heavy work will be done in
- * {@link SearchResults#getNextPage}.
+ * {@link SearchResults#getNextPageAsync}.
*
* @param queryExpression query string to search.
* @param searchSpec spec for setting document filters, adding projection, setting term
@@ -179,9 +249,9 @@
*
* <p>A usage report represents an event in which a user interacted with or viewed a document.
*
- * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
- * metrics for that particular document. These metrics are used for ordering {@link #search}
- * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and
+ * <p>For each call to {@link #reportUsageAsync}, AppSearch updates usage count and usage
+ * recency * metrics for that particular document. These metrics are used for ordering
+ * {@link #search} results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and
* {@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
*
* <p>Reporting usage of a document is optional.
@@ -191,14 +261,27 @@
* success.
*/
@NonNull
- ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request);
+ ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request);
+
+ /**
+ * @deprecated use {@link #reportUsageAsync}
+ *
+ * @param request The usage reporting request.
+ * @return The pending result of performing this operation which resolves to {@code null} on
+ * success.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+ return reportUsageAsync(request);
+ }
/**
* Removes {@link GenericDocument} objects by document IDs in a namespace from the
* {@link AppSearchSession} database.
*
* <p>Removed documents will no longer be surfaced by {@link #search} or
- * {@link #getByDocumentId}
+ * {@link #getByDocumentIdAsync}
* calls.
*
* <p>Once the database crosses the document count or byte usage threshold, removed documents
@@ -213,10 +296,28 @@
* {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
*/
@NonNull
- ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+ ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
@NonNull RemoveByDocumentIdRequest request);
/**
+ * @deprecated use {@link #removeAsync}
+ *
+ * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
+ * index.
+ * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+ * The keys of the {@link AppSearchBatchResult} represent the input IDs from the
+ * {@link RemoveByDocumentIdRequest} object. The values are either {@code null} on success,
+ * or a failed {@link AppSearchResult} otherwise. IDs that are not found will return a failed
+ * {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+ @NonNull RemoveByDocumentIdRequest request) {
+ return removeAsync(request);
+ }
+
+ /**
* Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
* match the {@code queryExpression} in given namespaces and schemaTypes which is set via
* {@link SearchSpec.Builder#addFilterNamespaces} and
@@ -234,7 +335,24 @@
* @return The pending result of performing this operation.
*/
@NonNull
- ListenableFuture<Void> remove(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+ ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec);
+
+ /**
+ * @deprecated use {@link #removeAsync}
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec containing schemaTypes, namespaces and query expression
+ * indicates how document will be removed. All specific about how to
+ * scoring, ordering, snippeting and resulting will be ignored.
+ * @return The pending result of performing this operation.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<Void> remove(@NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec) {
+ return removeAsync(queryExpression, searchSpec);
+ }
/**
* Gets the storage info for this {@link AppSearchSession} database.
@@ -245,7 +363,18 @@
* @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
*/
@NonNull
- ListenableFuture<StorageInfo> getStorageInfo();
+ ListenableFuture<StorageInfo> getStorageInfoAsync();
+
+ /**
+ * @deprecated use {@link #getStorageInfoAsync()}
+ *
+ * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<StorageInfo> getStorageInfo() {
+ return getStorageInfoAsync();
+ }
/**
* Flush all schema and document updates, additions, and deletes to disk if possible.
@@ -259,7 +388,21 @@
* save to disk.
*/
@NonNull
- ListenableFuture<Void> requestFlush();
+ ListenableFuture<Void> requestFlushAsync();
+
+ /**
+ * @deprecated use {@link #requestFlushAsync()}
+ *
+ * @return The pending result of performing this operation.
+ * {@link androidx.appsearch.exceptions.AppSearchException} with
+ * {@link AppSearchResult#RESULT_INTERNAL_ERROR} will be set to the future if we hit error when
+ * save to disk.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<Void> requestFlush() {
+ return requestFlushAsync();
+ }
/**
* Returns the {@link Features} to check for the availability of certain features
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index 26ab787..dc530ee 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -24,9 +24,9 @@
* <p>Features do not depend on any runtime state, and features will never be removed. Once
* {@link #isFeatureSupported} returns {@code true} for a certain feature, it is safe to assume that
* the feature will be available forever on that AppSearch storage implementation, at that
- * Android API level, on that device form factor.
- * <!--@exportToFramework:hide-->
+ * Android API level, on that device.
*/
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/Features.java)
public interface Features {
/**
@@ -38,10 +38,35 @@
/**
* Feature for {@link #isFeatureSupported(String)}. This feature covers
- * {@link GlobalSearchSession#addObserver} and
- * {@link GlobalSearchSession#removeObserver}.
+ * {@link GlobalSearchSession#registerObserverCallback} and
+ * {@link GlobalSearchSession#unregisterObserverCallback}.
*/
- String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+ String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK =
+ "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link GlobalSearchSession#getSchemaAsync}.
+ */
+ String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link GlobalSearchSession#getByDocumentIdAsync}.
+ */
+ String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},
+ * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},
+ * {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()},
+ * {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()},
+ * {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} and
+ * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility}
+ */
+ String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
/**
* Returns whether a feature is supported at run-time. Feature support depends on the
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index f1577f7..db06bda 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -51,8 +51,8 @@
* <p>Documents are constructed by using the {@link GenericDocument.Builder}.
* -->
*
- * @see AppSearchSession#put
- * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#putAsync
+ * @see AppSearchSession#getByDocumentIdAsync
* @see AppSearchSession#search
*/
public class GenericDocument {
@@ -81,7 +81,7 @@
*
* <p>Indexed properties are properties which are strings where the
* {@link AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other
- * than {@link AppSearchSchema.StringPropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
+ * than {@link AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}.
*/
public static int getMaxIndexedProperties() {
return MAX_INDEXED_PROPERTIES;
@@ -205,7 +205,7 @@
* time base, the document will be auto-deleted.
*
* <p>The default value is 0, which means the document is permanent and won't be auto-deleted
- * until the app is uninstalled or {@link AppSearchSession#remove} is called.
+ * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called.
*/
public long getTtlMillis() {
return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
@@ -1103,12 +1103,12 @@
* @param id the unique identifier for the {@link GenericDocument} in its namespace.
* @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
* provided {@code schemaType} must be defined using
- * {@link AppSearchSession#setSchema} prior
+ * {@link AppSearchSession#setSchemaAsync} prior
* to inserting a document of this {@code schemaType} into the
* AppSearch index using
- * {@link AppSearchSession#put}.
+ * {@link AppSearchSession#putAsync}.
* Otherwise, the document will be rejected by
- * {@link AppSearchSession#put} with result code
+ * {@link AppSearchSession#putAsync} with result code
* {@link AppSearchResult#RESULT_NOT_FOUND}.
*/
@SuppressWarnings("unchecked")
@@ -1177,7 +1177,7 @@
* Sets the schema type of this document, changing the value provided in the constructor.
*
* <p>To successfully index a document, the schema type must match the name of an
- * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
+ * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}.
* <!--@exportToFramework:hide-->
*/
@NonNull
@@ -1238,7 +1238,7 @@
* {@link System#currentTimeMillis} time base, the document will be auto-deleted.
*
* <p>The default value is 0, which means the document is permanent and won't be
- * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is
+ * auto-deleted until the app is uninstalled or {@link AppSearchSession#removeAsync} is
* called.
*
* @param ttlMillis a non-negative duration in milliseconds.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
index d9bfc0d..b2ebf3d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -34,7 +34,7 @@
* Encapsulates a request to retrieve documents by namespace and IDs from the
* {@link AppSearchSession} database.
*
- * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#getByDocumentIdAsync
*/
public final class GetByDocumentIdRequest {
/**
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
index f169099..fec585f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -16,21 +16,62 @@
package androidx.appsearch.app;
+import android.annotation.SuppressLint;
import android.os.Bundle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
-/** The response class of {@link AppSearchSession#getSchema} */
+/** The response class of {@link AppSearchSession#getSchemaAsync} */
public final class GetSchemaResponse {
private static final String VERSION_FIELD = "version";
private static final String SCHEMAS_FIELD = "schemas";
+ private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
+ "schemasNotDisplayedBySystem";
+ private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
+ private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD =
+ "schemasVisibleToPermissions";
+ private static final String ALL_REQUIRED_PERMISSION_FIELD =
+ "allRequiredPermission";
+ /**
+ * This Set contains all schemas that are not displayed by the system. All values in the set are
+ * prefixed with the package-database prefix. We do lazy fetch, the object will be created
+ * when the user first time fetch it.
+ */
+ @Nullable
+ private Set<String> mSchemasNotDisplayedBySystem;
+ /**
+ * This map contains all schemas and {@link PackageIdentifier} that has access to the schema.
+ * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the
+ * object will be created when the user first time fetch it.
+ */
+ @Nullable
+ private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+
+ /**
+ * This map contains all schemas and Android Permissions combinations that are required to
+ * access the schema. All keys in the map are prefixed with the package-database prefix. We
+ * do lazy fetch, the object will be created when the user first time fetch it.
+ * The Map is constructed in ANY-ALL cases. The querier could read the {@link GenericDocument}
+ * objects under the {@code schemaType} if they holds ALL required permissions of ANY
+ * combinations.
+ * The value set represents
+ * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
+ */
+ @Nullable
+ private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
private final Bundle mBundle;
@@ -60,7 +101,7 @@
/**
* Return the schemas most recently successfully provided to
- * {@link AppSearchSession#setSchema}.
+ * {@link AppSearchSession#setSchemaAsync}.
*
* <p>It is inefficient to call this method repeatedly.
*/
@@ -75,12 +116,161 @@
return schemas;
}
+ /**
+ * Returns all the schema types that are opted out of being displayed and visible on any
+ * system UI surface.
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @NonNull
+ public Set<String> getSchemaTypesNotDisplayedBySystem() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasNotDisplayedBySystem == null) {
+ List<String> schemasNotDisplayedBySystemList =
+ mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);
+ mSchemasNotDisplayedBySystem =
+ Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));
+ }
+ return mSchemasNotDisplayedBySystem;
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of packages that have access
+ * to that schema type.
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public Map<String, Set<PackageIdentifier>> getSchemaTypesVisibleToPackages() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasVisibleToPackages == null) {
+ Bundle schemaVisibleToPackagesBundle =
+ mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD);
+ Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+ for (String key : schemaVisibleToPackagesBundle.keySet()) {
+ List<Bundle> PackageIdentifierBundles = schemaVisibleToPackagesBundle
+ .getParcelableArrayList(key);
+ Set<PackageIdentifier> packageIdentifiers =
+ new ArraySet<>(PackageIdentifierBundles.size());
+ for (int i = 0; i < PackageIdentifierBundles.size(); i++) {
+ packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));
+ }
+ copy.put(key, packageIdentifiers);
+ }
+ mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
+ }
+ return mSchemasVisibleToPackages;
+ }
+
+ /**
+ * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+ * combinations that querier must hold to access that schema type.
+ *
+ * <p> The querier could read the {@link GenericDocument} objects under the {@code schemaType}
+ * if they holds ALL required permissions of ANY of the individual value sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+ * { PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.</li>
+ * <li>A querier holds both PermissionC and PermissionD has access.</li>
+ * <li>A querier holds only PermissionE has access.</li>
+ * <li>A querier holds both PermissionA and PermissionE has access.</li>
+ * <li>A querier holds only PermissionA doesn't have access.</li>
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+ * </ul>
+ *
+ * @return The map contains schema type and all combinations of required permission for querier
+ * to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS},
+ * {@link SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS},
+ * {@link SetSchemaRequest#READ_EXTERNAL_STORAGE},
+ * {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
+ * {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @SetSchemaRequest.AppSearchSupportedPermission
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasVisibleToPermissions == null) {
+ Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
+ Bundle schemaVisibleToPermissionBundle =
+ mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD);
+ for (String key : schemaVisibleToPermissionBundle.keySet()) {
+ ArrayList<Bundle> allRequiredPermissionsBundle =
+ schemaVisibleToPermissionBundle.getParcelableArrayList(key);
+ Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
+ if (allRequiredPermissionsBundle != null) {
+ // This should never be null
+ for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {
+ visibleToPermissions.add(new ArraySet<>(allRequiredPermissionsBundle.get(i)
+ .getIntegerArrayList(ALL_REQUIRED_PERMISSION_FIELD)));
+ }
+ }
+ copy.put(key, visibleToPermissions);
+ }
+ mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
+ }
+ return mSchemasVisibleToPermissions;
+ }
+
+ private void checkGetVisibilitySettingSupported() {
+ if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
+ throw new UnsupportedOperationException("Get visibility setting is not supported with"
+ + " this backend/Android API level combination.");
+ }
+ }
+
/** Builder for {@link GetSchemaResponse} objects. */
public static final class Builder {
private int mVersion = 0;
private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+ /**
+ * Creates the object when we actually set them. If we never set visibility settings, we
+ * should throw {@link UnsupportedOperationException} in the visibility getters.
+ */
+ @Nullable
+ private ArrayList<String> mSchemasNotDisplayedBySystem;
+ private Bundle mSchemasVisibleToPackages;
+ private Bundle mSchemasVisibleToPermissions;
private boolean mBuilt = false;
+ /** Create a {@link Builder} object} */
+ public Builder() {
+ this(/*getVisibilitySettingSupported=*/true);
+ }
+
+ /**
+ * Create a {@link Builder} object}.
+ *
+ * <p>This constructor should only be used in Android API below than T.
+ *
+ * @param getVisibilitySettingSupported whether supported
+ * {@link Features#ADD_PERMISSIONS_AND_GET_VISIBILITY} by this
+ * backend/Android API level.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(boolean getVisibilitySettingSupported) {
+ if (getVisibilitySettingSupported) {
+ mSchemasNotDisplayedBySystem = new ArrayList<>();
+ mSchemasVisibleToPackages = new Bundle();
+ mSchemasVisibleToPermissions = new Bundle();
+ }
+ }
+
/**
* Sets the database overall schema version.
*
@@ -102,12 +292,130 @@
return this;
}
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} will be displayed
+ * and visible on any system UI surface.
+ *
+ * @param schemaType The name of an {@link AppSearchSchema} within the same
+ * {@link GetSchemaResponse}, which won't be displayed by system.
+ */
+ // Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ resetIfBuilt();
+ if (mSchemasNotDisplayedBySystem == null) {
+ mSchemasNotDisplayedBySystem = new ArrayList<>();
+ }
+ mSchemasNotDisplayedBySystem.add(schemaType);
+ return this;
+ }
+
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} can be read by the
+ * specified package.
+ *
+ * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+ * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * <p>To opt into one-way data sharing with another application, the developer will need to
+ * explicitly grant the other application’s package name and certificate Read access to its
+ * data.
+ *
+ * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+ * one another.
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param packageIdentifiers Represents the package that has access to the given
+ * schema type.
+ */
+ // Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setSchemaTypeVisibleToPackages(
+ @NonNull String schemaType,
+ @NonNull Set<PackageIdentifier> packageIdentifiers) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(packageIdentifiers);
+ resetIfBuilt();
+ ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
+ for (PackageIdentifier packageIdentifier : packageIdentifiers) {
+ bundles.add(packageIdentifier.getBundle());
+ }
+ mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
+ return this;
+ }
+
+ /**
+ * Sets a set of required {@link android.Manifest.permission} combinations to the given
+ * schema type.
+ *
+ * <p> The querier could read the {@link GenericDocument} objects under the
+ * {@code schemaType} if they holds ALL required permissions of ANY of the individual value
+ * sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+ * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.</li>
+ * <li>A querier holds both PermissionC and PermissionD has access.</li>
+ * <li>A querier holds only PermissionE has access.</li>
+ * <li>A querier holds both PermissionA and PermissionE has access.</li>
+ * <li>A querier holds only PermissionA doesn't have access.</li>
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+ * </ul>
+ *
+ * @see android.Manifest.permission#READ_SMS
+ * @see android.Manifest.permission#READ_CALENDAR
+ * @see android.Manifest.permission#READ_CONTACTS
+ * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+ * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+ * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param visibleToPermissions The Android permissions that will be required to access
+ * the given schema.
+ */
+ // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setRequiredPermissionsForSchemaTypeVisibility(
+ @NonNull String schemaType,
+ @SetSchemaRequest.AppSearchSupportedPermission @NonNull
+ Set<Set<Integer>> visibleToPermissions) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(visibleToPermissions);
+ resetIfBuilt();
+ ArrayList<Bundle> visibleToPermissionsBundle = new ArrayList<>();
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ for (int permission : allRequiredPermissions) {
+ Preconditions.checkArgumentInRange(permission, SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+ }
+ Bundle allRequiredPermissionsBundle = new Bundle();
+ allRequiredPermissionsBundle.putIntegerArrayList(
+ ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));
+ visibleToPermissionsBundle.add(allRequiredPermissionsBundle);
+ }
+ mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,
+ visibleToPermissionsBundle);
+ return this;
+ }
+
/** Builds a {@link GetSchemaResponse} object. */
@NonNull
public GetSchemaResponse build() {
Bundle bundle = new Bundle();
bundle.putInt(VERSION_FIELD, mVersion);
bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
+ if (mSchemasNotDisplayedBySystem != null) {
+ // Only save the visibility fields if it was actually set.
+ bundle.putStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD,
+ mSchemasNotDisplayedBySystem);
+ bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);
+ bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);
+ }
mBuilt = true;
return new GetSchemaResponse(bundle);
}
@@ -115,6 +423,16 @@
private void resetIfBuilt() {
if (mBuilt) {
mSchemaBundles = new ArrayList<>(mSchemaBundles);
+ if (mSchemasNotDisplayedBySystem != null) {
+ // Only reset the visibility fields if it was actually set.
+ mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);
+ Bundle copyVisibleToPackages = new Bundle();
+ copyVisibleToPackages.putAll(mSchemasVisibleToPackages);
+ mSchemasVisibleToPackages = copyVisibleToPackages;
+ Bundle copyVisibleToPermissions = new Bundle();
+ copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);
+ mSchemasVisibleToPermissions = copyVisibleToPermissions;
+ }
mBuilt = false;
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
index 6784803..4ce54b4 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -16,9 +16,12 @@
// @exportToFramework:skipFile()
package androidx.appsearch.app;
+import android.annotation.SuppressLint;
+
import androidx.annotation.NonNull;
import androidx.annotation.RequiresFeature;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
import com.google.common.util.concurrent.ListenableFuture;
@@ -36,6 +39,31 @@
*/
public interface GlobalSearchSession extends Closeable {
/**
+ * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
+ * database name and identified by the namespace and ids in the request, from the
+ * {@link GlobalSearchSession} database. When a call is successful, the result will be
+ * returned in the successes section of the {@link AppSearchBatchResult} object in the callback.
+ * If the package doesn't exist, database doesn't exist, or if the calling package doesn't have
+ * access, these failures will be reflected as {@link AppSearchResult} objects with a
+ * RESULT_NOT_FOUND status code in the failures section of the {@link AppSearchBatchResult}
+ * object.
+ *
+ * @param packageName the name of the package to get from
+ * @param databaseName the name of the database to get from
+ * @param request a request containing a namespace and IDs of the documents to retrieve.
+ */
+ @NonNull
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)
+ // @exportToFramework:endStrip()
+ ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request);
+
+ /**
* Retrieves documents from all AppSearch databases that the querying application has access to.
*
* <p>Applications can be granted access to documents by specifying
@@ -51,7 +79,7 @@
* forming a query string.
*
* <p>This method is lightweight. The heavy work will be done in
- * {@link SearchResults#getNextPage}.
+ * {@link SearchResults#getNextPageAsync}.
*
* @param queryExpression query string to search.
* @param searchSpec spec for setting document filters, adding projection, setting term
@@ -64,11 +92,11 @@
/**
* Reports that a particular document has been used from a system surface.
*
- * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as
- * well as an API that can be used by the app itself.
+ * <p>See {@link AppSearchSession#reportUsageAsync} for a general description of document usage,
+ * as well as an API that can be used by the app itself.
*
* <p>Usage reported via this method is accounted separately from usage reported via
- * {@link AppSearchSession#reportUsage} and may be accessed using the constants
+ * {@link AppSearchSession#reportUsageAsync} and may be accessed using the constants
* {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
* {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
*
@@ -79,7 +107,70 @@
* is not part of the system.
*/
@NonNull
- ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request);
+ ListenableFuture<Void> reportSystemUsageAsync(@NonNull ReportSystemUsageRequest request);
+
+ /**
+ * @deprecated use {@link #reportSystemUsageAsync}
+ *
+ * @return The pending result of performing this operation which resolves to {@code null} on
+ * success. The pending result will be completed with an
+ * {@link androidx.appsearch.exceptions.AppSearchException} with a code of
+ * {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an app which
+ * is not part of the system.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+ return reportSystemUsageAsync(request);
+ }
+
+ /**
+ * Retrieves the collection of schemas most recently successfully provided to
+ * {@link AppSearchSession#setSchemaAsync} for any types belonging to the requested package and
+ * database that the caller has been granted access to.
+ *
+ * <p> If the requested package/database combination does not exist or the caller has not been
+ * granted access to it, then an empty GetSchemaResponse will be returned.
+ *
+ *
+ * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+ * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+ * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+ * access to or an empty GetSchemaResponse if the request package and database does not
+ * exist, has not set a schema or contains no schemas that are accessible to the caller.
+ */
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)
+ // @exportToFramework:endStrip()
+ ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+ @NonNull String databaseName);
+
+ /**
+ * @deprecated use {@link #getSchemaAsync}.
+ *
+ * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+ * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+ * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+ * access to or an empty GetSchemaResponse if the request package and database does not
+ * exist, has not set a schema or contains no schemas that are accessible to the caller.
+ */
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)
+ // @exportToFramework:endStrip()
+ @Deprecated
+ default ListenableFuture<GetSchemaResponse> getSchema(@NonNull String packageName,
+ @NonNull String databaseName) {
+ return getSchemaAsync(packageName, databaseName);
+ }
/**
* Returns the {@link Features} to check for the availability of certain features
@@ -89,63 +180,75 @@
Features getFeatures();
/**
- * Adds an {@link AppSearchObserverCallback} to monitor changes within the
- * databases owned by {@code observedPackage} if they match the given
+ * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+ * {@code targetPackageName} if they match the given
* {@link androidx.appsearch.observer.ObserverSpec}.
*
- * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} changes its schema visibility settings.
+ * <p>The observer callback is only triggered for data that changes after it is registered. No
+ * notification about existing data is sent as a result of registering an observer. To find out
+ * about existing data, you must use the {@link GlobalSearchSession#search} API.
*
- * <p>If no package matching {@code observedPackage} exists on the system, the registration call
- * will succeed but no notifications will be dispatched. Notifications could start flowing later
- * if {@code observedPackage} is installed and starts indexing data.
+ * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} changes its schema visibility settings.
+ *
+ * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} is installed and starts indexing data.
*
* <p>This feature may not be available in all implementations. Check
- * {@link Features#GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER} before calling this method.
+ * {@link Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
*
- * @param observedPackage Package whose changes to monitor
+ * @param targetPackageName Package whose changes to monitor
* @param spec Specification of what types of changes to listen for
* @param executor Executor on which to call the {@code observer} callback methods.
* @param observer Callback to trigger when a schema or document changes
+ * @throws AppSearchException if an error occurs trying to register the observer
* @throws UnsupportedOperationException if this feature is not available on this
* AppSearch implementation.
*/
// @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
- name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+ name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
// @exportToFramework:endStrip()
- void addObserver(
- @NonNull String observedPackage,
+ void registerObserverCallback(
+ @NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
- @NonNull AppSearchObserverCallback observer);
+ @NonNull ObserverCallback observer) throws AppSearchException;
/**
- * Removes previously registered {@link AppSearchObserverCallback} instances from the system.
+ * Removes previously registered {@link ObserverCallback} instances from the system.
*
- * <p>All instances of {@link AppSearchObserverCallback} which are equal to the provided
- * callback using {@link AppSearchObserverCallback#equals} will be removed.
+ * <p>All instances of {@link ObserverCallback} which are registered to observe
+ * {@code targetPackageName} and compare equal to the provided callback using the provided
+ * argument's {@link ObserverCallback#equals} will be removed.
*
* <p>If no matching observers have been registered, this method has no effect. If multiple
* matching observers have been registered, all will be removed.
*
* <p>This feature may not be available in all implementations. Check
- * {@link Features#GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER} before calling this method.
+ * {@link Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
*
- * @param observedPackage Package in which the observers to be removed are registered
- * @param observer Callback to unregister
+ * @param targetPackageName Package which the observers to be removed are listening to.
+ * @param observer Callback to unregister.
+ * @throws AppSearchException if an error occurs trying to remove the observer, such
+ * as a failure to communicate with the system service
+ * in the platform backend. Note that no
+ * error will be thrown if the provided observer
+ * doesn't match any registered observer.
* @throws UnsupportedOperationException if this feature is not available on this
* AppSearch implementation.
*/
// @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
- name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+ name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
// @exportToFramework:endStrip()
- void removeObserver(
- @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer);
+ void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+ throws AppSearchException;
/** Closes the {@link GlobalSearchSession}. */
@Override
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
index b47735b..e6a7b2c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
@@ -51,11 +51,11 @@
* higher version number than the current {@link AppSearchSchema} saved in AppSearch.
*
* <p>If this {@link Migrator} is provided to cover a compatible schema change via
- * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+ * {@link AppSearchSession#setSchemaAsync}, documents under the old version won't be removed
* unless you use the same document ID.
*
* <p>This method will be invoked on the background worker thread provided via
- * {@link AppSearchSession#setSchema}.
+ * {@link AppSearchSession#setSchemaAsync}.
*
* @param currentVersion The current version of the document's schema.
* @param finalVersion The final version that documents need to be migrated to.
@@ -74,7 +74,7 @@
* lower version number than the current {@link AppSearchSchema} saved in AppSearch.
*
* <p>If this {@link Migrator} is provided to cover a compatible schema change via
- * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+ * {@link AppSearchSession#setSchemaAsync}, documents under the old version won't be removed
* unless you use the same document ID.
*
* <p>This method will be invoked on the background worker thread.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
index 4d4a000..dff69b2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -37,7 +37,7 @@
* {@link GenericDocument}.
* <!--@exportToFramework:else()-->
*
- * @see AppSearchSession#put
+ * @see AppSearchSession#putAsync
*/
public final class PutDocumentsRequest {
private final List<GenericDocument> mDocuments;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
index addb96a..38be17a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -29,7 +29,7 @@
* Encapsulates a request to remove documents by namespace and IDs from the
* {@link AppSearchSession} database.
*
- * @see AppSearchSession#remove
+ * @see AppSearchSession#removeAsync
*/
public final class RemoveByDocumentIdRequest {
private final String mNamespace;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
index db26931..12422eb 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -23,9 +23,9 @@
* A request to report usage of a document owned by another app from a system UI surface.
*
* <p>Usage reported in this way is measured separately from usage reported via
- * {@link AppSearchSession#reportUsage}.
+ * {@link AppSearchSession#reportUsageAsync}.
*
- * <p>See {@link GlobalSearchSession#reportSystemUsage} for a detailed description of usage
+ * <p>See {@link GlobalSearchSession#reportSystemUsageAsync} for a detailed description of usage
* reporting.
*/
public final class ReportSystemUsageRequest {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
index 25edaf4..14b70c7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -22,9 +22,9 @@
/**
* A request to report usage of a document.
*
- * <p>See {@link AppSearchSession#reportUsage} for a detailed description of usage reporting.
+ * <p>See {@link AppSearchSession#reportUsageAsync} for a detailed description of usage reporting.
*
- * @see AppSearchSession#reportUsage
+ * @see AppSearchSession#reportUsageAsync
*/
public final class ReportUsageRequest {
private final String mNamespace;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index 7350991..17ed126 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -268,52 +268,68 @@
}
/**
- * This class represents a match objects for any Snippets that might be present in
- * {@link SearchResults} from query. Using this class
- * user can get the full text, exact matches and Snippets of document content for a given match.
+ * This class represents match objects for any Snippets that might be present in
+ * {@link SearchResults} from a query. Using this class, the user can get:
+ * <ul>
+ * <li>the full text - all of the text in that String property</li>
+ * <li>the exact term match - the 'term' (full word) that matched the query</li>
+ * <li>the subterm match - the portion of the matched term that appears in the query</li>
+ * <li>a suggested text snippet - a portion of the full text surrounding the exact term
+ * match, set to term boundaries. The size of the snippet is specified in
+ * {@link SearchSpec.Builder#setMaxSnippetSize}</li>
+ * </ul>
+ * for each match in the document.
*
* <p>Class Example 1:
- * A document contains following text in property subject:
- * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
+ * <p>A document contains the following text in property "subject":
+ * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar."
*
- * <p>If the queryExpression is "foo".
- *
- * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
- * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
- * nonsense word that’s used a lot is bar."
- * <p>{@link MatchInfo#getExactMatchRange()} returns [29, 32]
- * <p>{@link MatchInfo#getExactMatch()} returns "foo"
- * <p>{@link MatchInfo#getSubmatchRange()} returns [29, 32]
- * <p>{@link MatchInfo#getSubmatch()} returns "foo"
- * <p>{@link MatchInfo#getSnippetRange()} returns [26, 33]
- * <p>{@link MatchInfo#getSnippet()} returns "is foo."
+ * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10,
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "subject"</li>
+ * <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
+ * nonsense word that’s used a lot is bar."</li>
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32]</li>
+ * <li>{@link MatchInfo#getExactMatch()} returns "foo"</li>
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32]</li>
+ * <li>{@link MatchInfo#getSubmatch()} returns "foo"</li>
+ * <li>{@link MatchInfo#getSnippetRange()} returns [26, 33]</li>
+ * <li>{@link MatchInfo#getSnippet()} returns "is foo."</li>
+ * </ul>
* <p>
* <p>Class Example 2:
- * A document contains a property name sender which contains 2 property names name and email, so
- * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
- * <p>Let {@code sender.name = "Test Name Jr."} and
- * {@code sender.email = "TestNameJr@gmail.com"}
+ * <p>A document contains one property named "subject" and one property named "sender" which
+ * contains a "name" property.
*
- * <p>If the queryExpression is "Test". We will have 2 matches.
+ * In this case, we will have 2 property paths: {@code sender.name} and {@code subject}.
+ * <p>Let {@code sender.name = "Test Name Jr."} and
+ * {@code subject = "Testing 1 2 3"}
+ *
+ * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and
+ * {@link SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches:
*
* <p> Match-1
- * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
- * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
- * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 4]
- * <p>{@link MatchInfo#getExactMatch()} returns "Test"
- * <p>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
- * <p>{@link MatchInfo#getSubmatch()} returns "Test"
- * <p>{@link MatchInfo#getSnippetRange()} returns [0, 9]
- * <p>{@link MatchInfo#getSnippet()} returns "Test Name"
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "sender.name"</li>
+ * <li>{@link MatchInfo#getFullText()} returns "Test Name Jr."</li>
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4]</li>
+ * <li>{@link MatchInfo#getExactMatch()} returns "Test"</li>
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
+ * <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
+ * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
+ * <li>{@link MatchInfo#getSnippet()} returns "Test Name"</li>
+ * </ul>
* <p> Match-2
- * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
- * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
- * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 20]
- * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
- * <p>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
- * <p>{@link MatchInfo#getSubmatch()} returns "Test"
- * <p>{@link MatchInfo#getSnippetRange()} returns [0, 20]
- * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "subject"</li>
+ * <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3"</li>
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7]</li>
+ * <li>{@link MatchInfo#getExactMatch()} returns "Testing"</li>
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
+ * <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
+ * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
+ * <li>{@link MatchInfo#getSnippet()} returns "Testing 1"</li>
+ * </ul>
*/
public static final class MatchInfo {
/** The path of the matching snippet property. */
@@ -378,8 +394,10 @@
/**
* Gets the full text corresponding to the given entry.
- * <p>For class example this returns "A commonly used fake word is foo. Another nonsense
+ * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense
* word that's used a lot is bar."
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name Jr." and,
+ * for the second {@link MatchInfo}, this returns "Testing 1 2 3".
*/
@NonNull
public String getFullText() {
@@ -396,7 +414,7 @@
* Gets the {@link MatchRange} of the exact term of the given entry that matched the query.
* <p>Class example 1: this returns [29, 32].
* <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the
- * second {@link MatchInfo}, this returns [0, 20].
+ * second {@link MatchInfo}, this returns [0, 7].
*/
@NonNull
public MatchRange getExactMatchRange() {
@@ -412,7 +430,7 @@
* Gets the exact term of the given entry that matched the query.
* <p>Class example 1: this returns "foo".
* <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the
- * second {@link MatchInfo}, this returns "TestNameJr@gmail.com".
+ * second {@link MatchInfo}, this returns "Testing".
*/
@NonNull
public CharSequence getExactMatch() {
@@ -481,7 +499,7 @@
* {@link SearchSpec.Builder#setMaxSnippetSize}.
* <p>Class example 1: this returns [29, 41].
* <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 9] and, for the
- * second {@link MatchInfo}, this returns [0, 20].
+ * second {@link MatchInfo}, this returns [0, 13].
*/
@NonNull
public MatchRange getSnippetRange() {
@@ -501,7 +519,7 @@
* the matched token with content on either side clipped to token boundaries.
* <p>Class example 1: this returns "foo. Another".
* <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name" and, for
- * the second {@link MatchInfo}, this returns "TestNameJr@gmail.com".
+ * the second {@link MatchInfo}, this returns "Testing 1 2 3".
*/
@NonNull
public CharSequence getSnippet() {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
index 6bf301f..23b6b5d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
@@ -30,7 +30,7 @@
* objects, referred to as a "page", limited by the size configured by
* {@link SearchSpec.Builder#setResultCountPerPage}.
*
- * <p>To fetch a page of results, call {@link #getNextPage()}.
+ * <p>To fetch a page of results, call {@link #getNextPageAsync()}.
*
* <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
* results are fetched.
@@ -50,7 +50,18 @@
* objects.
*/
@NonNull
- ListenableFuture<List<SearchResult>> getNextPage();
+ ListenableFuture<List<SearchResult>> getNextPageAsync();
+
+ /**
+ * @deprecated use {@link #getNextPageAsync}.
+ * @return a {@link ListenableFuture} which resolves to a list of {@link SearchResult}
+ * objects.
+ */
+ @NonNull
+ @Deprecated
+ default ListenableFuture<List<SearchResult>> getNextPage() {
+ return getNextPageAsync();
+ }
@Override
void close();
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index e09eec9..94fc7d6 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -18,14 +18,18 @@
import android.annotation.SuppressLint;
+import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -76,13 +80,105 @@
* will be set into both {@link SetSchemaResponse#getIncompatibleTypes()} and
* {@link SetSchemaResponse#getMigratedTypes()}. See the migration section below.
* </ul>
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
* @see Migrator
*/
public final class SetSchemaRequest {
+
+ /**
+ * List of Android Permission are supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ *
+ * @see android.Manifest.permission
+ * @hide
+ */
+ @IntDef(value = {
+ READ_SMS,
+ READ_CALENDAR,
+ READ_CONTACTS,
+ READ_EXTERNAL_STORAGE,
+ READ_HOME_APP_SEARCH_DATA,
+ READ_ASSISTANT_APP_SEARCH_DATA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @interface AppSearchSupportedPermission {}
+
+ /**
+ * The {@link android.Manifest.permission#READ_SMS} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_SMS = 1;
+
+ /**
+ * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_CALENDAR = 2;
+
+ /**
+ * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_CONTACTS = 3;
+
+ /**
+ * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_EXTERNAL_STORAGE = 4;
+
+ /**
+ * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_HOME_APP_SEARCH_DATA = 5;
+
+ /**
+ * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
+
private final Set<AppSearchSchema> mSchemas;
private final Set<String> mSchemasNotDisplayedBySystem;
private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+ private final Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
private final Map<String, Migrator> mMigrators;
private final boolean mForceOverride;
private final int mVersion;
@@ -90,12 +186,14 @@
SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
@NonNull Set<String> schemasNotDisplayedBySystem,
@NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
+ @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
@NonNull Map<String, Migrator> migrators,
boolean forceOverride,
int version) {
mSchemas = Preconditions.checkNotNull(schemas);
mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
+ mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);
mMigrators = Preconditions.checkNotNull(migrators);
mForceOverride = forceOverride;
mVersion = version;
@@ -125,13 +223,47 @@
@NonNull
public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
- for (String key : mSchemasVisibleToPackages.keySet()) {
- copy.put(key, new ArraySet<>(mSchemasVisibleToPackages.get(key)));
+ for (Map.Entry<String, Set<PackageIdentifier>> entry :
+ mSchemasVisibleToPackages.entrySet()) {
+ copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
}
return copy;
}
/**
+ * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+ * combinations that querier must hold to access that schema type.
+ *
+ * <p> The querier could read the {@link GenericDocument} objects under the {@code schemaType}
+ * if they holds ALL required permissions of ANY of the individual value sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+ * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.</li>
+ * <li>A querier holds both PermissionC and PermissionD has access.</li>
+ * <li>A querier holds only PermissionE has access.</li>
+ * <li>A querier holds both PermissionA and PermissionE has access.</li>
+ * <li>A querier holds only PermissionA doesn't have access.</li>
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+ * </ul>
+ *
+ * <p>It’s inefficient to call this method repeatedly.
+ *
+ * @return The map contains schema type and all combinations of required permission for querier
+ * to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS},
+ * {@link SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS},
+ * {@link SetSchemaRequest#READ_EXTERNAL_STORAGE},
+ * {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
+ * {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+ */
+ @SetSchemaRequest.AppSearchSupportedPermission
+ @NonNull
+ public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+ return deepCopy(mSchemasVisibleToPermissions);
+ }
+
+ /**
* Returns the map of {@link Migrator}, the key will be the schema type of the
* {@link Migrator} associated with.
*/
@@ -174,6 +306,7 @@
private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
new ArrayMap<>();
+ private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
private boolean mForceOverride = false;
private int mVersion = DEFAULT_VERSION;
@@ -255,7 +388,7 @@
* and visible on any system UI surface.
*
* <p>This setting applies to the provided {@code schemaType} only, and does not persist
- * across {@link AppSearchSession#setSchema} calls.
+ * across {@link AppSearchSession#setSchemaAsync} calls.
*
* <p>The default behavior, if this method is not called, is to allow types to be
* displayed on system UI surfaces.
@@ -280,6 +413,72 @@
}
/**
+ * Adds a set of required Android {@link android.Manifest.permission} combination to the
+ * given schema type.
+ *
+ * <p> If the querier holds ALL of the required permissions in this combination, they will
+ * have access to read {@link GenericDocument} objects of the given schema type.
+ *
+ * <p> You can call this method to add multiple permission combinations, and the querier
+ * will have access if they holds ANY of the combinations.
+ *
+ * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
+ * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
+ * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
+ *
+ * @see android.Manifest.permission#READ_SMS
+ * @see android.Manifest.permission#READ_CALENDAR
+ * @see android.Manifest.permission#READ_CONTACTS
+ * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+ * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+ * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+ * @param schemaType The schema type to set visibility on.
+ * @param permissions A set of required Android permissions the caller need to hold
+ * to access {@link GenericDocument} objects that under the given
+ * schema.
+ * @throws IllegalArgumentException – if input unsupported permission.
+ */
+ // Merged list available from getRequiredPermissionsForSchemaTypeVisibility
+ @SuppressLint("MissingGetterMatchingBuilder")
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @NonNull
+ public Builder addRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType,
+ @AppSearchSupportedPermission @NonNull Set<Integer> permissions) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(permissions);
+ for (int permission : permissions) {
+ Preconditions.checkArgumentInRange(permission, READ_SMS,
+ READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+ }
+ resetIfBuilt();
+ Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
+ if (visibleToPermissions == null) {
+ visibleToPermissions = new ArraySet<>();
+ mSchemasVisibleToPermissions.put(schemaType, visibleToPermissions);
+ }
+ visibleToPermissions.add(permissions);
+ return this;
+ }
+
+ /** Clears all required permissions combinations for the given schema type. */
+ // @exportToFramework:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ // @exportToFramework:endStrip()
+ @NonNull
+ public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ resetIfBuilt();
+ mSchemasVisibleToPermissions.remove(schemaType);
+ return this;
+ }
+
+ /**
* Sets whether or not documents from the provided {@code schemaType} can be read by the
* specified package.
*
@@ -353,7 +552,7 @@
*
* @see SetSchemaRequest.Builder#setVersion
* @see SetSchemaRequest.Builder#addSchemas
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
*/
@NonNull
@SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
@@ -388,7 +587,7 @@
*
* @see SetSchemaRequest.Builder#setVersion
* @see SetSchemaRequest.Builder#addSchemas
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
*/
@NonNull
public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
@@ -406,7 +605,7 @@
* visible on any system UI surface.
*
* <p>This setting applies to the provided {@link androidx.appsearch.annotation.Document}
- * annotated class only, and does not persist across {@link AppSearchSession#setSchema}
+ * annotated class only, and does not persist across {@link AppSearchSession#setSchemaAsync}
* calls.
*
* <p>The default behavior, if this method is not called, is to allow types to be
@@ -469,6 +668,66 @@
return setSchemaTypeVisibilityForPackage(factory.getSchemaName(), visible,
packageIdentifier);
}
+
+ /**
+ * Adds a set of required Android {@link android.Manifest.permission} combination to the
+ * given schema type.
+ *
+ * <p> If the querier holds ALL of the required permissions in this combination, they will
+ * have access to read {@link GenericDocument} objects of the given schema type.
+ *
+ * <p> You can call this method to add multiple permission combinations, and the querier
+ * will have access if they holds ANY of the combinations.
+ *
+ * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
+ * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
+ * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
+ *
+ * @see android.Manifest.permission#READ_SMS
+ * @see android.Manifest.permission#READ_CALENDAR
+ * @see android.Manifest.permission#READ_CONTACTS
+ * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+ * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+ * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+ * @param documentClass The {@link androidx.appsearch.annotation.Document} class to set
+ * visibility on.
+ * @param permissions A set of required Android permissions the caller need to hold
+ * to access {@link GenericDocument} objects that under the given
+ * schema.
+ * @throws IllegalArgumentException – if input unsupported permission.
+ */
+ // Merged map available from getRequiredPermissionsForSchemaTypeVisibility
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ @NonNull
+ public Builder addRequiredPermissionsForDocumentClassVisibility(
+ @NonNull Class<?> documentClass,
+ @AppSearchSupportedPermission @NonNull Set<Integer> permissions)
+ throws AppSearchException {
+ Preconditions.checkNotNull(documentClass);
+ resetIfBuilt();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+ DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+ return addRequiredPermissionsForSchemaTypeVisibility(
+ factory.getSchemaName(), permissions);
+ }
+
+ /** Clears all required permissions combinations for the given schema type. */
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ @NonNull
+ public Builder clearRequiredPermissionsForDocumentClassVisibility(
+ @NonNull Class<?> documentClass)
+ throws AppSearchException {
+ Preconditions.checkNotNull(documentClass);
+ resetIfBuilt();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+ DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+ return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName());
+ }
// @exportToFramework:endStrip()
/**
@@ -497,7 +756,7 @@
*
* <p>Setting a version number that is different from the version number currently stored
* in AppSearch will result in AppSearch calling the {@link Migrator}s provided to
- * {@link AppSearchSession#setSchema} to migrate the documents already in AppSearch from
+ * {@link AppSearchSession#setSchemaAsync} to migrate the documents already in AppSearch from
* the previous version to the one set in this request. The version number can be
* updated without any other changes to the set of schemas.
*
@@ -513,7 +772,7 @@
*
* @throws IllegalArgumentException if the version is negative.
*
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
* @see Migrator
* @see SetSchemaRequest.Builder#setMigrator
*/
@@ -539,6 +798,7 @@
// Create a copy because we're going to remove from the set for verification purposes.
Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
+ referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
for (AppSearchSchema schema : mSchemas) {
referencedSchemas.remove(schema.getSchemaType());
@@ -558,6 +818,7 @@
mSchemas,
mSchemasNotDisplayedBySystem,
mSchemasVisibleToPackages,
+ mSchemasVisibleToPermissions,
mMigrators,
mForceOverride,
mVersion);
@@ -573,6 +834,8 @@
}
mSchemasVisibleToPackages = schemasVisibleToPackages;
+ mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
+
mSchemas = new ArraySet<>(mSchemas);
mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
mMigrators = new ArrayMap<>(mMigrators);
@@ -580,4 +843,17 @@
}
}
}
+
+ static ArrayMap<String, Set<Set<Integer>>> deepCopy(@NonNull Map<String,
+ Set<Set<Integer>>> original) {
+ ArrayMap<String, Set<Set<Integer>>> copy = new ArrayMap<>(original.size());
+ for (Map.Entry<String, Set<Set<Integer>>> entry : original.entrySet()) {
+ Set<Set<Integer>> valueCopy = new ArraySet<>();
+ for (Set<Integer> innerValue : entry.getValue()) {
+ valueCopy.add(new ArraySet<>(innerValue));
+ }
+ copy.put(entry.getKey(), valueCopy);
+ }
+ return copy;
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
index 88e6645..3f8ce5e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -30,7 +30,7 @@
import java.util.List;
import java.util.Set;
-/** The response class of {@link AppSearchSession#setSchema} */
+/** The response class of {@link AppSearchSession#setSchemaAsync} */
public class SetSchemaResponse {
private static final String DELETED_TYPES_FIELD = "deletedTypes";
@@ -102,7 +102,7 @@
*
* <p>A "deleted" type is a schema type that was previously a part of the database schema but
* was not present in the {@link SetSchemaRequest} object provided in the
- * {@link AppSearchSession#setSchema) call.
+ * {@link AppSearchSession#setSchemaAsync} call.
*
* <p>Documents for a deleted type are removed from the database.
*/
@@ -117,7 +117,7 @@
/**
* Returns a {@link Set} of schema type that were migrated by the
- * {@link AppSearchSession#setSchema} call.
+ * {@link AppSearchSession#setSchemaAsync} call.
*
* <p> A "migrated" type is a schema type that has triggered a {@link Migrator} instance to
* migrate documents of the schema type to another schema type, or to another version of the
@@ -139,13 +139,13 @@
/**
* Returns a {@link Set} of schema type whose new definitions set in the
- * {@link AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
+ * {@link AppSearchSession#setSchemaAsync} call were incompatible with the pre-existing schema.
*
* <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
* The type will also appear in {@link #getMigratedTypes()}.
*
* @see SetSchemaRequest
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
* @see SetSchemaRequest.Builder#setForceOverride
*/
@NonNull
@@ -281,7 +281,7 @@
/**
* The class represents a post-migrated {@link GenericDocument} that failed to be saved by
- * {@link AppSearchSession#setSchema}.
+ * {@link AppSearchSession#setSchemaAsync}.
*/
public static class MigrationFailure {
private static final String SCHEMA_TYPE_FIELD = "schemaType";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
new file mode 100644
index 0000000..6f04dc5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
@@ -0,0 +1,259 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Holds the visibility settings that apply to a schema type.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityDocument extends GenericDocument {
+ /**
+ * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+ */
+ public static final String SCHEMA_TYPE = "VisibilityType";
+ /** Namespace of documents that contain visibility settings */
+ public static final String NAMESPACE = "";
+
+ /**
+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+ */
+ private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+ /** Property that holds the package name that can access a schema. */
+ private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+ /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+ private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+ /** Property that holds the required permissions to access the schema. */
+ private static final String PERMISSION_PROPERTY = "permission";
+
+ // The initial schema version, one VisibilityDocument contains all visibility information for
+ // whole package.
+ public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+ // One VisibilityDocument contains visibility information for a single schema.
+ public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+ // One VisibilityDocument contains visibility information for a single schema.
+ public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+ public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+ */
+ public static final AppSearchSchema
+ SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(PERMISSION_PROPERTY,
+ VisibilityPermissionDocument.SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ public VisibilityDocument(@NonNull GenericDocument genericDocument) {
+ super(genericDocument);
+ }
+
+ public VisibilityDocument(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Returns whether this schema is visible to the system. */
+ public boolean isNotDisplayedBySystem() {
+ return getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+ }
+
+ /**
+ * Returns a package name array which could access this schema. Use {@link #getSha256Certs()}
+ * to get package's sha 256 certs. The same index of package names array and sha256Certs array
+ * represents same package.
+ */
+ @NonNull
+ public String[] getPackageNames() {
+ return getPropertyStringArray(PACKAGE_NAME_PROPERTY);
+ }
+
+ /**
+ * Returns a package sha256Certs array which could access this schema. Use
+ * {@link #getPackageNames()} to get package's name. The same index of package names array
+ * and sha256Certs array represents same package.
+ */
+ @NonNull
+ public byte[][] getSha256Certs() {
+ return getPropertyBytesArray(SHA_256_CERT_PROPERTY);
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema
+ * this {@link VisibilityDocument} represents.
+ */
+ @Nullable
+ public Set<Set<Integer>> getVisibleToPermissions() {
+ GenericDocument[] permissionDocuments = getPropertyDocumentArray(PERMISSION_PROPERTY);
+ if (permissionDocuments == null) {
+ return Collections.emptySet();
+ }
+ Set<Set<Integer>> visibleToPermissions = new ArraySet<>(permissionDocuments.length);
+ for (GenericDocument permissionDocument : permissionDocuments) {
+ visibleToPermissions.add(
+ new VisibilityPermissionDocument(permissionDocument)
+ .getAllRequiredPermissions());
+ }
+ return visibleToPermissions;
+ }
+
+ /** Builder for {@link VisibilityDocument}. */
+ public static class Builder extends GenericDocument.Builder<Builder> {
+ private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
+
+ /**
+ * Creates a {@link Builder} for a {@link VisibilityDocument}.
+ *
+ * @param id The SchemaType of the {@link AppSearchSchema} that this
+ * {@link VisibilityDocument} represents. The package and database prefix will be
+ * added in server side. We are using prefixed schema type to be the final id of
+ * this {@link VisibilityDocument}.
+ */
+ public Builder(@NonNull String id) {
+ super(NAMESPACE, id, SCHEMA_TYPE);
+ }
+
+ /** Sets whether this schema has opted out of platform surfacing. */
+ @NonNull
+ public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+ return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+ notDisplayedBySystem);
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @NonNull
+ public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
+ Preconditions.checkNotNull(packageIdentifiers);
+ mPackageIdentifiers.addAll(packageIdentifiers);
+ return this;
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @NonNull
+ public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+ Preconditions.checkNotNull(packageIdentifier);
+ mPackageIdentifiers.add(packageIdentifier);
+ return this;
+ }
+
+ /**
+ * Set required permission sets for a package needs to hold to the schema this
+ * {@link VisibilityDocument} represents.
+ *
+ * <p> The querier could have access if they holds ALL required permissions of ANY of the
+ * individual value sets.
+ */
+ @NonNull
+ public Builder setVisibleToPermissions(@NonNull Set<Set<Integer>> visibleToPermissions) {
+ Preconditions.checkNotNull(visibleToPermissions);
+ VisibilityPermissionDocument[] permissionDocuments =
+ new VisibilityPermissionDocument[visibleToPermissions.size()];
+ int i = 0;
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ permissionDocuments[i++] = new VisibilityPermissionDocument
+ .Builder(NAMESPACE, /*id=*/String.valueOf(i))
+ .setVisibleToAllRequiredPermissions(allRequiredPermissions)
+ .build();
+ }
+ setPropertyDocument(PERMISSION_PROPERTY, permissionDocuments);
+ return this;
+ }
+
+ /** Build a {@link VisibilityDocument} */
+ @Override
+ @NonNull
+ public VisibilityDocument build() {
+ String[] packageNames = new String[mPackageIdentifiers.size()];
+ byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];
+ int i = 0;
+ for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {
+ packageNames[i] = packageIdentifier.getPackageName();
+ sha256Certs[i] = packageIdentifier.getSha256Certificate();
+ ++i;
+ }
+ setPropertyString(PACKAGE_NAME_PROPERTY, packageNames);
+ setPropertyBytes(SHA_256_CERT_PROPERTY, sha256Certs);
+ return new VisibilityDocument(super.build());
+ }
+ }
+
+
+ /** Build the List of {@link VisibilityDocument} from visibility settings. */
+ @NonNull
+ public static List<VisibilityDocument> toVisibilityDocuments(
+ @NonNull SetSchemaRequest setSchemaRequest) {
+ Set<AppSearchSchema> searchSchemas = setSchemaRequest.getSchemas();
+ Set<String> schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();
+ Map<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+ setSchemaRequest.getSchemasVisibleToPackages();
+ Map<String, Set<Set<Integer>>> schemasVisibleToPermissions =
+ setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();
+
+ List<VisibilityDocument> visibilityDocuments = new ArrayList<>(searchSchemas.size());
+
+ for (AppSearchSchema searchSchema : searchSchemas) {
+ String schemaType = searchSchema.getSchemaType();
+ VisibilityDocument.Builder documentBuilder =
+ new VisibilityDocument.Builder(/*id=*/searchSchema.getSchemaType());
+ documentBuilder.setNotDisplayedBySystem(
+ schemasNotDisplayedBySystem.contains(schemaType));
+
+ if (schemasVisibleToPackages.containsKey(schemaType)) {
+ documentBuilder.addVisibleToPackages(schemasVisibleToPackages.get(schemaType));
+ }
+
+ if (schemasVisibleToPermissions.containsKey(schemaType)) {
+ documentBuilder.setVisibleToPermissions(
+ schemasVisibleToPermissions.get(schemaType));
+ }
+ visibilityDocuments.add(documentBuilder.build());
+ }
+ return visibilityDocuments;
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
new file mode 100644
index 0000000..e859cb9
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+
+import java.util.Set;
+
+/**
+ * The nested document that holds all required permissions for a caller need to hold to access the
+ * schema which the outer {@link VisibilityDocument} represents.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityPermissionDocument extends GenericDocument {
+
+ /**
+ * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+ */
+ public static final String SCHEMA_TYPE = "VisibilityPermissionType";
+
+ /** Property that holds the required permissions to access the schema. */
+ private static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update {@link VisibilityDocument#SCHEMA_VERSION_LATEST}.
+ */
+ public static final AppSearchSchema
+ SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.LongPropertyConfig
+ .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ VisibilityPermissionDocument(@NonNull GenericDocument genericDocument) {
+ super(genericDocument);
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema
+ * that the outer {@link VisibilityDocument} represents.
+ */
+ @Nullable
+ public Set<Integer> getAllRequiredPermissions() {
+ return toInts(getPropertyLongArray(ALL_REQUIRED_PERMISSIONS_PROPERTY));
+ }
+
+ /** Builder for {@link VisibilityPermissionDocument}. */
+ public static class Builder extends GenericDocument.Builder<Builder> {
+
+ /**
+ * Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id) {
+ super(namespace, id, SCHEMA_TYPE);
+ }
+
+ /** Sets whether this schema has opted out of platform surfacing. */
+ @NonNull
+ public Builder setVisibleToAllRequiredPermissions(
+ @NonNull Set<Integer> allRequiredPermissions) {
+ setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, toLongs(allRequiredPermissions));
+ return this;
+ }
+
+ /** Build a {@link VisibilityPermissionDocument} */
+ @Override
+ @NonNull
+ public VisibilityPermissionDocument build() {
+ return new VisibilityPermissionDocument(super.build());
+ }
+ }
+
+ @NonNull
+ static long[] toLongs(@NonNull Set<Integer> properties) {
+ long[] outputs = new long[properties.size()];
+ int i = 0;
+ for (int property : properties) {
+ outputs[i++] = property;
+ }
+ return outputs;
+ }
+
+ @Nullable
+ private static Set<Integer> toInts(@Nullable long[] properties) {
+ if (properties == null) {
+ return null;
+ }
+ Set<Integer> outputs = new ArraySet<>(properties.length);
+ for (long property : properties) {
+ outputs.add((int) property);
+ }
+ return outputs;
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
index c040aa2..c01917e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
@@ -16,23 +16,13 @@
package androidx.appsearch.observer;
-import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
/**
- * An interface which apps can implement to subscribe to notifications of changes to AppSearch data.
+ * @deprecated use {@link ObserverCallback} instead.
+ * @hide
*/
-public interface AppSearchObserverCallback {
- /**
- * Callback to trigger after schema changes (schema type added, updated or removed).
- *
- * @param changeInfo Information about the nature of the change.
- */
- void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo);
-
- /**
- * Callback to trigger after document changes (documents added, updated or removed).
- *
- * @param changeInfo Information about the nature of the change.
- */
- void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo);
-}
+// TODO(b/209734214): Remove this after dogfooders and devices have migrated away from this class.
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchObserverCallback extends ObserverCallback {}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
index 0fd9ae2..32d19ad 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
@@ -21,8 +21,11 @@
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
+import java.util.Collections;
+import java.util.Set;
+
/**
- * Contains information about an individual change detected by an {@link AppSearchObserverCallback}.
+ * Contains information about an individual change detected by an {@link ObserverCallback}.
*
* <p>This class reports information about document changes, i.e. when documents were added, updated
* or removed.
@@ -30,8 +33,11 @@
* <p>Changes are grouped by package, database, schema type and namespace. Each unique
* combination of these items will generate a unique {@link DocumentChangeInfo}.
*
+ * <p>Notifications are only sent for documents whose schema type matches an observer's schema
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}).
+ *
* <p>Note that document changes that happen during schema migration from calling
- * {@link androidx.appsearch.app.AppSearchSession#setSchema} are not reported via this class.
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync} are not reported via this class.
* Such changes are reported through {@link SchemaChangeInfo}.
*/
public final class DocumentChangeInfo {
@@ -39,8 +45,7 @@
private final String mDatabase;
private final String mNamespace;
private final String mSchemaName;
-
- // TODO(b/193494000): Add the set of changed document IDs to this class
+ private final Set<String> mChangedDocumentIds;
/**
* Constructs a new {@link DocumentChangeInfo}.
@@ -49,16 +54,21 @@
* @param database The database in which the documents that changed reside.
* @param namespace The namespace in which the documents that changed reside.
* @param schemaName The name of the schema type that contains the changed documents.
+ * @param changedDocumentIds The set of document IDs that have been changed as part of this
+ * notification.
*/
public DocumentChangeInfo(
@NonNull String packageName,
@NonNull String database,
@NonNull String namespace,
- @NonNull String schemaName) {
+ @NonNull String schemaName,
+ @NonNull Set<String> changedDocumentIds) {
mPackageName = Preconditions.checkNotNull(packageName);
mDatabase = Preconditions.checkNotNull(database);
mNamespace = Preconditions.checkNotNull(namespace);
mSchemaName = Preconditions.checkNotNull(schemaName);
+ mChangedDocumentIds = Collections.unmodifiableSet(
+ Preconditions.checkNotNull(changedDocumentIds));
}
/** Returns the package name of the app which owns the documents that changed. */
@@ -85,6 +95,16 @@
return mSchemaName;
}
+ /**
+ * Returns the set of document IDs that have been changed as part of this notification.
+ *
+ * <p>This will never be empty.
+ */
+ @NonNull
+ public Set<String> getChangedDocumentIds() {
+ return mChangedDocumentIds;
+ }
+
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
@@ -93,12 +113,14 @@
return mPackageName.equals(that.mPackageName)
&& mDatabase.equals(that.mDatabase)
&& mNamespace.equals(that.mNamespace)
- && mSchemaName.equals(that.mSchemaName);
+ && mSchemaName.equals(that.mSchemaName)
+ && mChangedDocumentIds.equals(that.mChangedDocumentIds);
}
@Override
public int hashCode() {
- return ObjectsCompat.hash(mPackageName, mDatabase, mNamespace, mSchemaName);
+ return ObjectsCompat.hash(
+ mPackageName, mDatabase, mNamespace, mSchemaName, mChangedDocumentIds);
}
@NonNull
@@ -109,6 +131,7 @@
+ ", database='" + mDatabase + '\''
+ ", namespace='" + mNamespace + '\''
+ ", schemaName='" + mSchemaName + '\''
+ + ", changedDocumentIds='" + mChangedDocumentIds + '\''
+ '}';
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.java
new file mode 100644
index 0000000..cae945a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 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 androidx.appsearch.observer;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An interface which apps can implement to subscribe to notifications of changes to AppSearch data.
+ */
+public interface ObserverCallback {
+ /**
+ * Callback to trigger after schema changes (schema type added, updated or removed).
+ *
+ * @param changeInfo Information about the nature of the change.
+ */
+ void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo);
+
+ /**
+ * Callback to trigger after document changes (documents added, updated or removed).
+ *
+ * @param changeInfo Information about the nature of the change.
+ */
+ void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo);
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
index e8979e5..6e3705e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
@@ -37,8 +37,8 @@
import java.util.Set;
/**
- * Configures the types, namespaces and other properties that {@link AppSearchObserverCallback}
- * instances match against.
+ * Configures the types, namespaces and other properties that {@link ObserverCallback} instances
+ * match against.
*/
public final class ObserverSpec {
private static final String FILTER_SCHEMA_FIELD = "filterSchema";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
index 6eff8c4..e912cf8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
@@ -21,11 +21,15 @@
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
+import java.util.Collections;
+import java.util.Set;
+
/**
- * Contains information about a schema change detected by an {@link AppSearchObserverCallback}.
+ * Contains information about a schema change detected by an {@link ObserverCallback}.
*
* <p>This object will be sent when a schema type having a name matching an observer's schema
- * type filters has been added, updated, or removed.
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}) has been added, updated, or
+ * removed.
*
* <p>Note that schema changes may cause documents to be migrated or removed. When this happens,
* individual document updates will NOT be dispatched via {@link DocumentChangeInfo}. The only
@@ -35,18 +39,23 @@
public final class SchemaChangeInfo {
private final String mPackageName;
private final String mDatabaseName;
-
- // TODO(b/193494000): Add the set of changed schema names to this class
+ private final Set<String> mChangedSchemaNames;
/**
* Constructs a new {@link SchemaChangeInfo}.
*
* @param packageName The package name of the app which owns the schema that changed.
* @param databaseName The database in which the schema that changed resides.
+ * @param changedSchemaNames Names of schemas that have changed as part of this notification.
*/
- public SchemaChangeInfo(@NonNull String packageName, @NonNull String databaseName) {
+ public SchemaChangeInfo(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull Set<String> changedSchemaNames) {
mPackageName = Preconditions.checkNotNull(packageName);
mDatabaseName = Preconditions.checkNotNull(databaseName);
+ mChangedSchemaNames = Collections.unmodifiableSet(
+ Preconditions.checkNotNull(changedSchemaNames));
}
/** Returns the package name of the app which owns the schema that changed. */
@@ -61,17 +70,29 @@
return mDatabaseName;
}
+ /**
+ * Returns the names of schema types affected by this change notification.
+ *
+ * <p>This will never be empty.
+ */
+ @NonNull
+ public Set<String> getChangedSchemaNames() {
+ return mChangedSchemaNames;
+ }
+
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (!(o instanceof SchemaChangeInfo)) return false;
SchemaChangeInfo that = (SchemaChangeInfo) o;
- return mPackageName.equals(that.mPackageName) && mDatabaseName.equals(that.mDatabaseName);
+ return mPackageName.equals(that.mPackageName)
+ && mDatabaseName.equals(that.mDatabaseName)
+ && mChangedSchemaNames.equals(that.mChangedSchemaNames);
}
@Override
public int hashCode() {
- return ObjectsCompat.hash(mPackageName, mDatabaseName);
+ return ObjectsCompat.hash(mPackageName, mDatabaseName, mChangedSchemaNames);
}
@NonNull
@@ -80,6 +101,7 @@
return "SchemaChangeInfo{"
+ "packageName='" + mPackageName + '\''
+ ", databaseName='" + mDatabaseName + '\''
+ + ", changedSchemaNames='" + mChangedSchemaNames + '\''
+ '}';
}
}
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index e3d4c59..48ec64b 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -71,7 +71,7 @@
'../../../prebuilts/tools/common/google-java-format/google-java-format')
# Miscellaneous constants
-CHANGEID_FILE_NAME = 'synced_jetpack_changeid.txt'
+SHA_FILE_NAME = 'synced_jetpack_sha.txt'
class ExportToFramework:
@@ -96,6 +96,10 @@
print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
return
+ copyToPath = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
+ if copyToPath:
+ dest_path = os.path.join(self._framework_appsearch_root, copyToPath.group(1))
+
print('Copy: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
if transform_func:
contents = transform_func(contents)
@@ -133,7 +137,7 @@
flags=re.MULTILINE)
# Apply in-place replacements
- return (contents
+ contents = (contents
.replace('androidx.appsearch.app', 'android.app.appsearch')
.replace(
'androidx.appsearch.localstorage.',
@@ -161,11 +165,20 @@
.replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
.replace('Preconditions.checkNotNull(', 'Objects.requireNonNull(')
.replace('ObjectsCompat.', 'Objects.')
+
.replace('/*@exportToFramework:CurrentTimeMillisLong*/', '@CurrentTimeMillisLong')
.replace('/*@exportToFramework:UnsupportedAppUsage*/', '@UnsupportedAppUsage')
.replace('<!--@exportToFramework:hide-->', '@hide')
.replace('// @exportToFramework:skipFile()', '')
)
+ contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
+
+ # Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
+ # to allow the same documentation to compile for both.
+ contents = re.sub(r'(#[a-zA-Z0-9_]+)Async}', r'\1}', contents)
+ contents = re.sub(
+ r'(\@see [^#]+#[a-zA-Z0-9_]+)Async$', r'\1', contents, flags=re.MULTILINE)
+ return contents
def _TransformTestCode(self, contents):
contents = (contents
@@ -318,19 +331,26 @@
self._ExportImplCode()
self._FormatWrittenFiles()
- def WriteChangeIdFile(self, changeid):
- """Copies the changeid of the most recent public CL into a file on the framework side.
+ def WriteShaFile(self, sha):
+ """Copies the git sha of the most recent public CL into a file on the framework side.
This file is used for tracking, to determine what framework is synced to.
- You must always provide a changeid of an exported, preferably even submitted CL. If you
- abandon the CL pointed to by this changeid, the next person syncing framework will be unable
- to find what CL it is synced to.
+ You must always provide a sha of a submitted submitted git commit. If you abandon the CL
+ pointed to by this sha, the next person syncing framework will be unable to find what CL it
+ is synced to.
+
+ The previous content of the sha file, if any, is returned.
"""
- file_path = os.path.join(self._framework_appsearch_root, CHANGEID_FILE_NAME)
+ file_path = os.path.join(self._framework_appsearch_root, SHA_FILE_NAME)
+ old_sha = None
+ if os.path.isfile(file_path):
+ with open(file_path, 'r') as fh:
+ old_sha = fh.read().rstrip()
with open(file_path, 'w') as fh:
- print(changeid, file=fh)
+ print(sha, file=fh)
print('Wrote "%s"' % file_path)
+ return old_sha
if __name__ == '__main__':
@@ -339,6 +359,12 @@
sys.argv[0]),
file=sys.stderr)
sys.exit(1)
+ if sys.argv[2].startswith('I'):
+ print('Error: Git sha "%s" looks like a changeid. Please provide a git sha instead.' % (
+ sys.argv[2]),
+ file=sys.stderr)
+ sys.exit(1)
+
source_dir = os.path.normpath(os.path.dirname(sys.argv[0]))
dest_dir = os.path.normpath(sys.argv[1])
dest_dir = os.path.join(dest_dir, 'packages/modules/AppSearch')
@@ -349,4 +375,10 @@
sys.exit(1)
exporter = ExportToFramework(source_dir, dest_dir)
exporter.ExportCode()
- exporter.WriteChangeIdFile(sys.argv[2])
+
+ # Update the sha file
+ new_sha = sys.argv[2]
+ old_sha = exporter.WriteShaFile(new_sha)
+ if old_sha and old_sha != new_sha:
+ print('Command to diff old version to new version:')
+ print(' git log %s..%s -- appsearch/' % (old_sha, new_sha))
diff --git a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
index 862a9ff..88036ff 100644
--- a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
@@ -381,6 +381,7 @@
* @hide
*/
@RestrictTo(LIBRARY)
+ @SuppressWarnings("deprecation")
@NonNull
public static List<ResolveInfo> getBrowserActionsIntentHandlers(@NonNull Context context) {
Intent intent =
@@ -389,7 +390,7 @@
return pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
}
- @SuppressWarnings("NullAway") // TODO: b/141869398
+ @SuppressWarnings({"NullAway", "deprecation"}) // TODO: b/141869398
private static void openFallbackBrowserActionsMenu(Context context, Intent intent) {
Uri uri = intent.getData();
ArrayList<Bundle> bundles = intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
index 40386b6..f1675f2 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
@@ -86,6 +86,7 @@
* {@link CustomTabsIntent#EXTRA_SESSION}.
* @return The token that was generated.
*/
+ @SuppressWarnings("deprecation")
public static @Nullable CustomTabsSessionToken getSessionTokenFromIntent(
@NonNull Intent intent) {
Bundle b = intent.getExtras();
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java b/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
index b95bd78..37d604e 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
@@ -119,6 +119,7 @@
* @return Whether the specified Custom Tabs provider supports the specified splash screen
* feature/version.
*/
+ @SuppressWarnings("deprecation")
public static boolean areSplashScreensSupported(@NonNull Context context,
@NonNull String packageName, @NonNull String version) {
Intent serviceIntent = new Intent()
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index f3fd01e..4a5ac59 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -274,6 +274,7 @@
assertNullSessionInExtras(intent);
}
+ @SuppressWarnings("deprecation")
@Test
public void putsSessionBinderAndId_IfSuppliedInConstructor() {
CustomTabsSession session = TestUtil.makeMockSession();
@@ -283,6 +284,7 @@
assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
}
+ @SuppressWarnings("deprecation")
@Test
public void putsSessionBinderAndId_IfSuppliedInSetter() {
CustomTabsSession session = TestUtil.makeMockSession();
@@ -292,6 +294,7 @@
assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
}
+ @SuppressWarnings("deprecation")
@Test
public void putsPendingSessionId() {
CustomTabsSession.PendingSession pendingSession = TestUtil.makeMockPendingSession();
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
index 318a99a..fa0c171 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
@@ -55,6 +55,7 @@
return PendingIntent.getBroadcast(mock(Context.class), 0, new Intent(), 0);
}
+ @SuppressWarnings("deprecation")
public static void assertIntentHasSession(@NonNull Intent intent,
@NonNull CustomTabsSession session) {
assertEquals(session.getBinder(), intent.getExtras().getBinder(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index f58237b..0940177 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -269,6 +269,10 @@
// Reenable after upgradingto 7.1.0-beta01
disable.add("SupportAnnotationUsage")
+ // Broken when building with compileSdk 31, b/208451611
+ disable.add("VectorPath")
+ disable.add("InvalidVectorPath")
+
// Provide stricter enforcement for project types intended to run on a device.
if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
fatal.add("Assert")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index 48197b5..22e79ea 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -35,7 +35,7 @@
* Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
* "android-28" or "android-Q") as you would see within the SDK's platforms directory.
*/
- const val COMPILE_SDK_VERSION = "android-31"
+ const val COMPILE_SDK_VERSION = "android-Tiramisu"
/**
* The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
index c3a2051..89dbe9f 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
@@ -48,7 +48,7 @@
private val imageValidationActivityRequestCode =
ImageValidationActivity::class.java.hashCode() % 1000
- @Suppress("UNCHECKED_CAST")
+ @Suppress("UNCHECKED_CAST", "DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.full_listview)
diff --git a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
index 04e9656..0b75733 100644
--- a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
+++ b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
@@ -121,7 +121,7 @@
Integer desiredResult = 5;
Bundleable desiredBundleable = Bundleable.create(desiredResult);
int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
- Integer unsupportedResult = new Integer(-1);
+ Integer unsupportedResult = -1;
String param = "param";
Bundleable paramBundle = Bundleable.create(param);
@@ -193,7 +193,7 @@
public void addListener_multiple_listener_multiple_param() throws BundlerException,
RemoteException {
int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
- Integer unsupportedResult = new Integer(-1);
+ Integer unsupportedResult = -1;
CarResultStubMap<Integer, Integer> carResultStubMap = new CarResultStubMap<>(
desiredResultType,
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
index 2036e2b..d502dd9 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
@@ -30,6 +30,7 @@
import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarIconSpan;
import androidx.car.app.model.CarLocation;
import androidx.car.app.model.Distance;
import androidx.car.app.model.DistanceSpan;
@@ -93,6 +94,17 @@
0,
1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (index == 4) {
+ description.setSpan(CarIconSpan.create(new CarIcon.Builder(
+ IconCompat.createWithBitmap(
+ BitmapFactory.decodeResource(
+ carContext.getResources(),
+ R.drawable.ic_hi)))
+ .build(), CarIconSpan.ALIGN_BOTTOM),
+ 5,
+ 6,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
boolean isBrowsable = index > mPlaces.size() / 2;
@@ -225,52 +237,84 @@
.build()));
Location location5 = new Location(SamplePlaces.class.getSimpleName());
- location5.setLatitude(47.6490374);
- location5.setLongitude(-122.3527127);
+ location5.setLatitude(37.422014);
+ location5.setLongitude(-122.084776);
+ SpannableString title5 = new SpannableString(" ");
+ title5.setSpan(CarIconSpan.create(new CarIcon.Builder(
+ IconCompat.createWithBitmap(
+ BitmapFactory.decodeResource(
+ carContext.getResources(),
+ R.drawable.ic_hi)))
+ .build(), CarIconSpan.ALIGN_BOTTOM),
+ 0,
+ 1,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ SpannableString description5 = new SpannableString(" ");
+ places.add(
+ new PlaceInfo(
+ title5,
+ "1600 Amphitheatre Pkwy, Mountain View, CA 94043",
+ description5,
+ "+16502530000",
+ location5,
+ new PlaceMarker.Builder()
+ .setIcon(
+ new CarIcon.Builder(
+ IconCompat.createWithBitmap(
+ BitmapFactory.decodeResource(
+ carContext.getResources(),
+ R.drawable.test_image_square)))
+ .build(),
+ PlaceMarker.TYPE_IMAGE)
+ .build()));
+
+ Location location6 = new Location(SamplePlaces.class.getSimpleName());
+ location6.setLatitude(47.6490374);
+ location6.setLongitude(-122.3527127);
places.add(
new PlaceInfo(
"Google Bothell",
"11831 North Creek Pkwy, Bothell, WA 98011",
"Text label",
"n/a",
- location5,
+ location6,
new PlaceMarker.Builder().build()));
// Some hosts may display more items in the list than others, so create 3 more items.
- Location location6 = new Location(SamplePlaces.class.getSimpleName());
- location6.setLatitude(47.5496056);
- location6.setLongitude(-122.2571713);
+ Location location7 = new Location(SamplePlaces.class.getSimpleName());
+ location7.setLatitude(47.5496056);
+ location7.setLongitude(-122.2571713);
places.add(
new PlaceInfo(
"Seward Park",
"5900 Lake Washington Blvd S, Seattle, WA 98118",
"Text label",
"n/a",
- location6,
+ location7,
new PlaceMarker.Builder().build()));
- Location location7 = new Location(SamplePlaces.class.getSimpleName());
- location7.setLatitude(47.5911456);
- location7.setLongitude(-122.2256602);
+ Location location8 = new Location(SamplePlaces.class.getSimpleName());
+ location8.setLatitude(47.5911456);
+ location8.setLongitude(-122.2256602);
places.add(
new PlaceInfo(
"Luther Burbank Park",
"2040 84th Ave SE, Mercer Island, WA 98040",
"Text label",
"n/a",
- location7,
+ location8,
new PlaceMarker.Builder().build()));
- Location location8 = new Location(SamplePlaces.class.getSimpleName());
- location8.setLatitude(47.6785932);
- location8.setLongitude(-122.2113821);
+ Location location9 = new Location(SamplePlaces.class.getSimpleName());
+ location9.setLatitude(47.6785932);
+ location9.setLongitude(-122.2113821);
places.add(
new PlaceInfo(
"Heritage Park",
"111 Waverly Way, Kirkland, WA 98033",
"Text label",
"n/a",
- location8,
+ location9,
new PlaceMarker.Builder().build()));
return places;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 101fe8c..14d0d83 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -314,7 +314,7 @@
if (titleText.isEmpty()) {
throw new IllegalArgumentException("The title cannot be null or empty");
}
- CarTextConstraints.TEXT_ONLY.validateOrThrow(titleText);
+ CarTextConstraints.TEXT_AND_ICON.validateOrThrow(titleText);
mTitle = titleText;
return this;
}
@@ -334,7 +334,7 @@
if (requireNonNull(title).isEmpty()) {
throw new IllegalArgumentException("The title cannot be null or empty");
}
- CarTextConstraints.TEXT_ONLY.validateOrThrow(title);
+ CarTextConstraints.TEXT_AND_ICON.validateOrThrow(title);
mTitle = title;
return this;
}
@@ -410,7 +410,7 @@
@NonNull
public Builder addText(@NonNull CharSequence text) {
CarText carText = CarText.create(requireNonNull(text));
- CarTextConstraints.TEXT_WITH_COLORS.validateOrThrow(carText);
+ CarTextConstraints.TEXT_WITH_COLORS_AND_ICON.validateOrThrow(carText);
mTexts.add(CarText.create(requireNonNull(text)));
return this;
}
@@ -424,7 +424,7 @@
*/
@NonNull
public Builder addText(@NonNull CarText text) {
- CarTextConstraints.TEXT_WITH_COLORS.validateOrThrow(requireNonNull(text));
+ CarTextConstraints.TEXT_WITH_COLORS_AND_ICON.validateOrThrow(requireNonNull(text));
mTexts.add(text);
return this;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
index bba4a06..2346dcb 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
@@ -89,6 +89,15 @@
DurationSpan.class,
ForegroundCarColorSpan.class));
+ /** Allow text with color and icon {@link CarSpan}s. */
+ @NonNull
+ public static final CarTextConstraints TEXT_WITH_COLORS_AND_ICON =
+ new CarTextConstraints(Arrays.asList(
+ DistanceSpan.class,
+ DurationSpan.class,
+ ForegroundCarColorSpan.class,
+ CarIconSpan.class));
+
private final HashSet<Class<? extends CarSpan>> mAllowedTypes;
/**
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
index 9a9bd33..49c96d4 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
@@ -47,6 +47,7 @@
public class CarAppNotificationBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = LogTags.TAG + ".NBR";
+ @SuppressWarnings("deprecation")
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
ComponentName appComponent =
diff --git a/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java b/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
index 5a03bb1..86daab0 100644
--- a/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
@@ -52,6 +52,7 @@
private final ComponentName mComponentName = new ComponentName(mContext, CarAppService.class);
private final Intent mIntent = new Intent("fooAction").setComponent(mComponentName);
+ @SuppressWarnings("deprecation")
@Test
public void getCarApp_returnsTheExpectedPendingIntent() throws PendingIntent.CanceledException {
PendingIntent pendingIntent = CarPendingIntent.getCarApp(mContext, 1, mIntent,
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index e88b101..2adb329 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -43,7 +43,7 @@
implementation("androidx.compose.runtime:runtime-saveable:1.1.0-rc01")
implementation(project(":compose:ui:ui-util"))
implementation(libs.kotlinStdlib)
- implementation("androidx.core:core:1.5.0")
+ implementation("androidx.core:core:1.7.0")
implementation('androidx.collection:collection:1.0.0')
testImplementation(project(":compose:ui:ui-test-font"))
@@ -121,7 +121,7 @@
androidMain.dependencies {
api("androidx.annotation:annotation:1.1.0")
- implementation("androidx.core:core:1.5.0")
+ implementation("androidx.core:core:1.7.0")
implementation('androidx.collection:collection:1.0.0')
}
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
index f85edaa..aed16bd 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
@@ -312,7 +312,7 @@
String propertyName = "backgroundColor";
int startColor = Color.RED;
int endColor = Color.BLUE;
- Object[] values = {new Integer(startColor), new Integer(endColor)};
+ Object[] values = {startColor, endColor};
ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
ObjectAnimator colorAnimator = ObjectAnimator.ofObject(object, propertyName,
evaluator, values);
@@ -360,7 +360,7 @@
String propertyName = "backgroundColor";
int startColor = Color.RED;
int endColor = Color.BLUE;
- Object[] values = {new Integer(startColor), new Integer(endColor)};
+ Object[] values = {startColor, endColor};
ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
ObjectAnimator colorAnimator = ObjectAnimator.ofObject(object, propertyName,
evaluator, values);
diff --git a/core/core-animation/build.gradle b/core/core-animation/build.gradle
index 0b22cb9..45da746 100644
--- a/core/core-animation/build.gradle
+++ b/core/core-animation/build.gradle
@@ -25,6 +25,7 @@
api("androidx.annotation:annotation:1.2.0")
implementation("androidx.core:core:1.3.1")
implementation("androidx.collection:collection:1.1.0")
+ implementation("androidx.tracing:tracing:1.0.0")
androidTestImplementation(libs.testExtJunit, excludes.espresso)
androidTestImplementation(libs.testRules, excludes.espresso)
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
index 35db26b..4654d57 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
@@ -27,7 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.core.os.TraceCompat;
+import androidx.tracing.Trace;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -1164,7 +1164,7 @@
}
// mReversing needs to be reset *after* notifying the listeners for the end callbacks.
mReversing = false;
- TraceCompat.endSection();
+ Trace.endSection();
}
/**
@@ -1173,7 +1173,7 @@
*/
private void startAnimation() {
- TraceCompat.beginSection(getNameForTrace());
+ Trace.beginSection(getNameForTrace());
mAnimationEndRequested = false;
initAnimation();
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index d712487..fd87143 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -662,6 +662,7 @@
@SdkSuppress(minSdkVersion = 29)
@LargeTest
@Test
+ @SuppressWarnings("deprecation")
public void testFixedAllFileChecksumsSingleThread() throws Exception {
installPackage(TEST_FIXED_APK);
assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
@@ -951,6 +952,7 @@
trustedInstallers);
}
+ @SuppressWarnings("deprecation")
private Checksum[] getFileChecksums(@NonNull String packageName,
@Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
throws Exception {
@@ -1184,6 +1186,7 @@
}
}
+ @SuppressWarnings("deprecation")
static Certificate getInstallerCertificate(Context context) throws Exception {
PackageManager pm = context.getPackageManager();
PackageInfo installerPackageInfo = pm.getPackageInfo(INSTALLER_PACKAGE_NAME,
diff --git a/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java b/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
index d739e9b..122e0ab 100644
--- a/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
+++ b/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
@@ -72,7 +72,9 @@
/**
* The name of the dialer role.
* <p>
- * To qualify for this role, an application needs to handle the intent to dial:
+ * To qualify for this role, an application needs to handle the intent to dial, and implement
+ * an {@link android.telecom.InCallService} if the application targets
+ * {@link android.os.Build.VERSION_CODES.TIRAMISU} or higher:
* <pre class="prettyprint">{@code
* <activity>
* <intent-filter>
@@ -85,6 +87,16 @@
* <data android:scheme="tel" />
* </intent-filter>
* </activity>
+ * <service android:permission="android.permission.BIND_INCALL_SERVICE">
+ * <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
+ * <meta-data
+ * android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI"
+ * android:value="false" />
+ * <intent-filter>
+ * <action android:name="android.telecom.InCallService" />
+ * </intent-filter>
+ * </service>
+ *
* }</pre>
* The application will be able to handle those intents by default, and gain access to phone,
* contacts, SMS, microphone and camera.
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index ac52137..83fef5e 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1010,10 +1010,15 @@
method public static <T> T? getSystemService(android.content.Context, Class<T!>);
method public static String? getSystemServiceName(android.content.Context, Class<?>);
method public static boolean isDeviceProtectedStorage(android.content.Context);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
method public static boolean startActivities(android.content.Context, android.content.Intent![]);
method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
method public static void startForegroundService(android.content.Context, android.content.Intent);
+ field public static final int RECEIVER_EXPORTED = 2; // 0x2
+ field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+ field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
}
public class FileProvider extends android.content.ContentProvider {
@@ -1735,6 +1740,7 @@
method public java.util.Locale? getFirstMatch(String![]);
method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
method public boolean isEmpty();
+ method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
method @IntRange(from=0) public int size();
method public String toLanguageTags();
method public Object? unwrap();
@@ -1753,7 +1759,18 @@
}
public final class ParcelCompat {
+ method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static boolean readBoolean(android.os.Parcel);
+ method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+ method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+ method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+ method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+ method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static void writeBoolean(android.os.Parcel, boolean);
}
@@ -3022,6 +3039,9 @@
method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3118,6 +3138,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+ method public String? getUniqueId();
method public String! getViewIdResourceName();
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
method public int getWindowId();
@@ -3211,6 +3232,7 @@
method public void setTraversalAfter(android.view.View!, int);
method public void setTraversalBefore(android.view.View!);
method public void setTraversalBefore(android.view.View!, int);
+ method public void setUniqueId(String?);
method public void setViewIdResourceName(String!);
method public void setVisibleToUser(boolean);
method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3272,6 +3294,9 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index abd1c17..70ec372 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1010,10 +1010,15 @@
method public static <T> T? getSystemService(android.content.Context, Class<T!>);
method public static String? getSystemServiceName(android.content.Context, Class<?>);
method public static boolean isDeviceProtectedStorage(android.content.Context);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
method public static boolean startActivities(android.content.Context, android.content.Intent![]);
method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
method public static void startForegroundService(android.content.Context, android.content.Intent);
+ field public static final int RECEIVER_EXPORTED = 2; // 0x2
+ field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+ field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
}
public class FileProvider extends android.content.ContentProvider {
@@ -1691,7 +1696,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
method @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
- method @ChecksSdkIntAtLeast(codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
+ method @ChecksSdkIntAtLeast(api=33) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
}
@RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public static @interface BuildCompat.PrereleaseSdkCheck {
@@ -1740,6 +1745,7 @@
method public java.util.Locale? getFirstMatch(String![]);
method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
method public boolean isEmpty();
+ method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
method @IntRange(from=0) public int size();
method public String toLanguageTags();
method public Object? unwrap();
@@ -1758,7 +1764,18 @@
}
public final class ParcelCompat {
+ method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static boolean readBoolean(android.os.Parcel);
+ method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+ method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+ method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+ method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+ method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static void writeBoolean(android.os.Parcel, boolean);
}
@@ -3027,6 +3044,9 @@
method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3123,6 +3143,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+ method public String? getUniqueId();
method public String! getViewIdResourceName();
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
method public int getWindowId();
@@ -3216,6 +3237,7 @@
method public void setTraversalAfter(android.view.View!, int);
method public void setTraversalBefore(android.view.View!);
method public void setTraversalBefore(android.view.View!, int);
+ method public void setUniqueId(String?);
method public void setViewIdResourceName(String!);
method public void setVisibleToUser(boolean);
method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3277,6 +3299,9 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index ec250b5..12ad90c 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1118,10 +1118,15 @@
method public static <T> T? getSystemService(android.content.Context, Class<T!>);
method public static String? getSystemServiceName(android.content.Context, Class<?>);
method public static boolean isDeviceProtectedStorage(android.content.Context);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+ method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
method public static boolean startActivities(android.content.Context, android.content.Intent![]);
method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
method public static void startForegroundService(android.content.Context, android.content.Intent);
+ field public static final int RECEIVER_EXPORTED = 2; // 0x2
+ field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+ field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
}
public class FileProvider extends android.content.ContentProvider {
@@ -2069,6 +2074,7 @@
method public java.util.Locale? getFirstMatch(String![]);
method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
method public boolean isEmpty();
+ method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
method @IntRange(from=0) public int size();
method public String toLanguageTags();
method public Object? unwrap();
@@ -2087,7 +2093,18 @@
}
public final class ParcelCompat {
+ method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static boolean readBoolean(android.os.Parcel);
+ method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+ method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+ method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+ method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+ method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+ method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
method public static void writeBoolean(android.os.Parcel, boolean);
}
@@ -3478,6 +3495,9 @@
method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType int);
method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+ field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3506,7 +3526,7 @@
field @Deprecated public static final int TYPE_WINDOW_CONTENT_CHANGED = 2048; // 0x800
}
- @IntDef(flag=true, value={androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_TEXT, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface AccessibilityEventCompat.ContentChangeType {
+ @IntDef(flag=true, value={androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_TEXT, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_STARTED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_DROPPED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_CANCELLED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface AccessibilityEventCompat.ContentChangeType {
}
public final class AccessibilityManagerCompat {
@@ -3579,6 +3599,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+ method public String? getUniqueId();
method public String! getViewIdResourceName();
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
method public int getWindowId();
@@ -3672,6 +3693,7 @@
method public void setTraversalAfter(android.view.View!, int);
method public void setTraversalBefore(android.view.View!);
method public void setTraversalBefore(android.view.View!, int);
+ method public void setUniqueId(String?);
method public void setViewIdResourceName(String!);
method public void setVisibleToUser(boolean);
method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3737,6 +3759,9 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index ae9dd30..ad0606f 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -1247,28 +1247,6 @@
<issue
id="BanUncheckedReflection"
message="Calling `Method.invoke` without an SDK check"
- errorLine1=" return (IBinder) sGetIBinderMethod.invoke(bundle, key);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/BundleCompat.java"
- line="60"
- column="38"/>
- </issue>
-
- <issue
- id="BanUncheckedReflection"
- message="Calling `Method.invoke` without an SDK check"
- errorLine1=" sPutIBinderMethod.invoke(bundle, key, binder);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/BundleCompat.java"
- line="84"
- column="21"/>
- </issue>
-
- <issue
- id="BanUncheckedReflection"
- message="Calling `Method.invoke` without an SDK check"
errorLine1=" return (String) getMethod.invoke(systemProperties, name);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -3105,259 +3083,6 @@
<issue
id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeCustomAnimation(context,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="69"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeScaleUpAnimation("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="99"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeClipRevealAnimation("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="123"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeThumbnailScaleUpAnimation("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="152"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeSceneTransitionAnimation("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="180"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" ActivityOptions.makeSceneTransitionAnimation(activity, pairs));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="217"
- column="37"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeTaskLaunchBehind());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="235"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return new ActivityOptionsCompatImpl(ActivityOptions.makeBasic());"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="247"
- column="66"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mActivityOptions.requestUsageTimeReport(receiver);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="277"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 24; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mActivityOptions.setLaunchBounds(screenSpacePixelRect));"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="288"
- column="38"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 24; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return mActivityOptions.getLaunchBounds();"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
- line="296"
- column="37"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, showIntent),"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
- line="62"
- column="26"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, showIntent),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
- line="62"
- column="40"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
- line="120"
- column="26"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 19; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" alarmManager.setExact(type, triggerAtMillis, operation);"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
- line="165"
- column="26"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
- line="225"
- column="26"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return AppOpsManager.permissionToOp(permission);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="79"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 19; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return appOpsManager.noteOp(op, uid, packageName);"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="110"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 19; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return appOpsManager.noteOpNoThrow(op, uid, packageName);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="130"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="160"
- column="51"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return appOpsManager.noteProxyOp(op, proxiedPackageName);"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="161"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="179"
- column="51"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return appOpsManager.noteProxyOpNoThrow(op, proxiedPackageName);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
- line="180"
- column="34"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
message="This call references a method added in API level 17; however, the containing class androidx.core.graphics.BitmapCompat is reachable from earlier API levels and will fail run-time class verification."
errorLine1=" return bitmap.hasMipMap();"
errorLine2=" ~~~~~~~~~">
@@ -3402,28 +3127,6 @@
<issue
id="ClassVerificationFailure"
- message="This call references a method added in API level 18; however, the containing class androidx.core.app.BundleCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return bundle.getBinder(key);"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/BundleCompat.java"
- line="106"
- column="27"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 18; however, the containing class androidx.core.app.BundleCompat is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" bundle.putBinder(key, binder);"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/BundleCompat.java"
- line="122"
- column="20"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
message="This call references a method added in API level 16; however, the containing class androidx.core.os.CancellationSignal is reachable from earlier API levels and will fail run-time class verification."
errorLine1=" ((android.os.CancellationSignal) obj).cancel();"
errorLine2=" ~~~~~~">
@@ -3490,6 +3193,215 @@
<issue
id="ClassVerificationFailure"
+ message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .getDisplay(displayId);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+ line="86"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .getDisplays();"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+ line="106"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .getDisplays(category);"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+ line="135"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setAutoMirrored(mirrored);"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="79"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.isAutoMirrored();"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="96"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setHotspot(x, y);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="111"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setHotspotBounds(left, top, right, bottom);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="124"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setTint(tint);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="136"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setTintList(tint);"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="150"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.setTintMode(tintMode);"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="164"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.getAlpha();"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="178"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.applyTheme(theme);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="189"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.canApplyTheme();"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="198"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.getColorFilter();"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="211"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" clearColorFilter(((InsetDrawable) drawable).getDrawable());"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="232"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" child = state.getChild(i);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="242"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" drawable.inflate(res, parser, attrs, theme);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="269"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 23; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.setLayoutDirection(layoutDirection);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="355"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 23; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return drawable.getLayoutDirection();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+ line="392"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
message="This call references a method added in API level 28; however, the containing class androidx.core.app.DialogCompat is reachable from earlier API levels and will fail run-time class verification."
errorLine1=" return dialog.requireViewById(id);"
errorLine2=" ~~~~~~~~~~~~~~~">
@@ -3622,39 +3534,6 @@
<issue
id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" return sourceActivity.shouldUpRecreateTask(targetIntent);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/NavUtils.java"
- line="61"
- column="35"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" sourceActivity.navigateUpTo(upIntent);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/NavUtils.java"
- line="109"
- column="28"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" Intent result = sourceActivity.getParentActivityIntent();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/app/NavUtils.java"
- line="132"
- column="44"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
message="This call references a method added in API level 26; however, the containing class androidx.core.app.NotificationChannelCompat is reachable from earlier API levels and will fail run-time class verification."
errorLine1=" this(channel.getId(), channel.getImportance());"
errorLine2=" ~~~~~">
diff --git a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
index 9800531..1968a78 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.eq;
@@ -31,7 +32,9 @@
import android.support.v4.BaseInstrumentationTestCase;
import android.view.View;
+import androidx.annotation.OptIn;
import androidx.core.app.ActivityCompat.PermissionCompatDelegate;
+import androidx.core.os.BuildCompat;
import androidx.core.test.R;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -145,4 +148,13 @@
ActivityCompat.requireViewById(getActivity(), View.NO_ID);
}
+ @Test
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void testShouldShowRequestPermissionRationaleForPostNotifications() throws Throwable {
+ if (!BuildCompat.isAtLeastT()) {
+ // permission doesn't exist yet, so should return false
+ assertFalse(ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),
+ Manifest.permission.POST_NOTIFICATIONS));
+ }
+ }
}
diff --git a/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java b/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
index 9d5b663..3119729 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
@@ -23,6 +23,7 @@
import android.os.Build;
import android.support.v4.BaseInstrumentationTestCase;
+import androidx.core.os.BuildCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -90,6 +91,16 @@
shouldNotDumpSpecialArgOnVersion("--translation", Build.VERSION_CODES.S);
}
+ @Test
+ public void testShouldDumpInternalState_listDumpables() {
+ shouldNotDumpSpecialArgOnT("--list-dumpables");
+ }
+
+ @Test
+ public void testShouldDumpInternalState_dumpDumpable() {
+ shouldNotDumpSpecialArgOnT("--dump-dumpable");
+ }
+
private void shouldNotDumpSpecialArgOnVersion(String specialArg, int minApiVersion) {
String[] args = { specialArg };
int actualApiVersion = Build.VERSION.SDK_INT;
@@ -103,6 +114,19 @@
}
}
+ private void shouldNotDumpSpecialArgOnT(String specialArg) {
+ String[] args = { specialArg };
+ int actualApiVersion = Build.VERSION.SDK_INT;
+
+ if (BuildCompat.isAtLeastT()) {
+ assertFalse(specialArg + " should be skipped on API " + actualApiVersion,
+ mComponentActivity.shouldDumpInternalState(args));
+ } else {
+ assertTrue(specialArg + " should be ignored on API " + actualApiVersion,
+ mComponentActivity.shouldDumpInternalState(args));
+ }
+ }
+
private class NeverAddedExtraData extends ComponentActivity.ExtraData {
}
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index af95506..ed0ed4d 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -264,6 +264,7 @@
Log.i(TAG, "Running!");
}
+ @SuppressWarnings("deprecation")
@Override
protected void onHandleWork(@NonNull Intent intent) {
Log.i(TAG, "Handling work: " + intent);
diff --git a/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
index 0acf64e..90f0788 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
@@ -38,7 +38,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-
+@SuppressWarnings("deprecation")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ShareCompatTest extends BaseInstrumentationTestCase<TestActivity> {
@@ -54,6 +54,7 @@
super(TestActivity.class);
}
+ @SuppressWarnings("deprecation")
@Test
public void testBuilder() {
Activity activity = mActivityTestRule.getActivity();
@@ -71,6 +72,7 @@
activity.getComponentName());
}
+ @SuppressWarnings("deprecation")
@Test
public void testBuilderWithoutActivity() {
Context context = mActivityTestRule.getActivity().getApplicationContext();
@@ -86,6 +88,7 @@
assertNull(intent.getParcelableExtra(ShareCompat.EXTRA_CALLING_ACTIVITY_INTEROP));
}
+ @SuppressWarnings("deprecation")
@Test
public void testBuilderWithWrappedActivity() {
Activity activity = mActivityTestRule.getActivity();
@@ -104,6 +107,7 @@
activity.getComponentName());
}
+ @SuppressWarnings("deprecation")
@Test
@SdkSuppress(minSdkVersion = 16)
public void testBuilderSingleStreamUri() {
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index e55d822..afb8249 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -70,8 +70,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import android.Manifest;
import android.accounts.AccountManager;
import android.app.ActivityManager;
import android.app.AlarmManager;
@@ -87,9 +93,12 @@
import android.app.usage.UsageStatsManager;
import android.appwidget.AppWidgetManager;
import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.RestrictionsManager;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
@@ -134,6 +143,9 @@
import android.view.inputmethod.InputMethodManager;
import android.view.textservice.TextServicesManager;
+import androidx.annotation.OptIn;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.os.BuildCompat;
import androidx.core.test.R;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
@@ -146,7 +158,14 @@
@LargeTest
public class ContextCompatTest extends BaseInstrumentationTestCase<ThemedYellowActivity> {
private Context mContext;
+ private IntentFilter mTestFilter = new IntentFilter();
+ private String mPermission;
+ private BroadcastReceiver mTestReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ }
+ };
public ContextCompatTest() {
super(ThemedYellowActivity.class);
}
@@ -154,6 +173,7 @@
@Before
public void setup() {
mContext = mActivityTestRule.getActivity();
+ mPermission = mContext.getPackageName() + ".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
}
@Test
@@ -445,6 +465,71 @@
return ((size * tdensity) + (sdensity >> 1)) / sdensity;
}
+ @Test
+ public void testRegisterReceiver_noExportStateFlagThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> ContextCompat.registerReceiver(mContext,
+ mTestReceiver, mTestFilter, 0));
+
+ assertThrows(IllegalArgumentException.class, () -> ContextCompat.registerReceiver(mContext,
+ mTestReceiver, mTestFilter, Context.RECEIVER_VISIBLE_TO_INSTANT_APPS));
+ }
+
+ @Test
+ public void testRegisterReceiver_specifyBothExportStateFlagsThrowsException() {
+ assertThrows(IllegalArgumentException.class,
+ () -> ContextCompat.registerReceiver(mContext,
+ mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_EXPORTED | ContextCompat.RECEIVER_NOT_EXPORTED));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33)
+ public void testRegisterReceiverApi33() {
+ Context spyContext = spy(mContext);
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null),
+ any(), eq(ContextCompat.RECEIVER_NOT_EXPORTED));
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any(),
+ eq(ContextCompat.RECEIVER_EXPORTED));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26, maxSdkVersion = 32)
+ public void testRegisterReceiverApi26() {
+ Context spyContext = spy(mContext);
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter),
+ eq(mPermission), any());
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any(),
+ eq(0));
+
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = 25)
+ public void testRegisterReceiver() {
+ Context spyContext = spy(mContext);
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(mPermission),
+ any());
+
+ ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+ ContextCompat.RECEIVER_EXPORTED);
+ verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any());
+ }
+
@Test(expected = NullPointerException.class)
public void testCheckSelfPermissionNull() {
ContextCompat.checkSelfPermission(mContext, null);
@@ -474,4 +559,22 @@
ContextCompat.checkSelfPermission(mContext,
android.Manifest.permission.DELETE_PACKAGES));
}
+
+ @Test
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void testCheckSelfPermissionNotificationPermission() {
+ if (BuildCompat.isAtLeastT()) {
+ assertEquals(
+ mContext.checkCallingPermission(Manifest.permission.POST_NOTIFICATIONS),
+ ContextCompat.checkSelfPermission(
+ mContext,
+ Manifest.permission.POST_NOTIFICATIONS));
+ } else {
+ assertEquals("Notification permission allowed by default on devices <= SDK 32",
+ NotificationManagerCompat.from(mContext).areNotificationsEnabled()
+ ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED,
+ ContextCompat.checkSelfPermission(mContext,
+ Manifest.permission.POST_NOTIFICATIONS));
+ }
+ }
}
diff --git a/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
index 98a9aea..d612ba4 100644
--- a/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
@@ -318,6 +318,7 @@
* this case, they are permission revocation apps.
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ @SuppressWarnings("deprecation")
static void setupPermissionRevocationApps(
PackageManager packageManager, List<String> packageNames) {
List<ResolveInfo> resolveInfos = new ArrayList<>();
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
index 53f5500..bc98c5f 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
@@ -333,6 +333,7 @@
}
}
+ @Suppress("DEPRECATION")
private fun mockPackageManager() = mockThrowOnUnmocked<PackageManager> {
val mockCerts = params.mockCerts
whenever(getPackageInfo(TEST_PKG_NAME, params.mockCerts.flag)) {
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
index b4eb56a..6bab3ca 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
@@ -89,7 +89,7 @@
import java.util.Collections;
import java.util.List;
-@SuppressWarnings("unchecked")
+@SuppressWarnings({"unchecked", "deprecation"})
@RunWith(AndroidJUnit4.class)
public class ShortcutManagerCompatTest extends BaseInstrumentationTestCase<TestActivity> {
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
index 0ee4d9a..6efc56f 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
@@ -59,6 +59,7 @@
import java.util.concurrent.TimeUnit;
@SmallTest
+@SuppressWarnings("deprecation")
public class TypefaceCompatTest {
public Context mContext;
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java b/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
index 28d9898..60d3c11 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
@@ -57,6 +57,7 @@
import java.io.OutputStream;
import java.util.Arrays;
+@SuppressWarnings("deprecation")
@RunWith(AndroidJUnit4.class)
@SmallTest
public class IconCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
index 3fd5f6d..6bd9800 100644
--- a/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
@@ -393,6 +393,44 @@
assertNotEquals(first.toLanguageTags(), second.toLanguageTags());
}
+ @SdkSuppress(minSdkVersion = 21)
+ @Test
+ public void testLocaleListCompat_matchesLanguageAndScript() {
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("fr-Latn-FR"),
+ forLanguageTag("fr-Latn")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hans-CN"),
+ forLanguageTag("zh-Hans")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hant-TW"),
+ forLanguageTag("zh-Hant")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+ forLanguageTag("en-US")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+ forLanguageTag("en-CA")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("ar-NA"),
+ forLanguageTag("ar-ZA")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+ forLanguageTag("zh")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+ forLanguageTag("zh-Hans")));
+ assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+ forLanguageTag("zh-Hant")));
+
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hant-TW"),
+ forLanguageTag("zh-Hans")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-XA"),
+ forLanguageTag("en-US")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("ar-YE"),
+ forLanguageTag("ar-XB")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+ forLanguageTag("zh-TW")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+ forLanguageTag("zh")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+ forLanguageTag("zh-Hant")));
+ assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+ forLanguageTag("zh-Hans")));
+ }
+
private Locale forLanguageTag(String str) {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(str);
diff --git a/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
index e1d273b..873a9c2 100644
--- a/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
@@ -16,17 +16,31 @@
package androidx.core.os;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import android.content.pm.Signature;
+import android.graphics.Rect;
+import android.os.Build;
import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Objects;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ParcelCompatTest {
@@ -40,4 +54,186 @@
assertTrue(ParcelCompat.readBoolean(p));
assertFalse(ParcelCompat.readBoolean(p));
}
+
+ @Test
+ public void readParcelable2Arg() {
+ Rect r = new Rect(0, 0, 10, 10);
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(r, 0);
+
+ p.setDataPosition(0);
+ Rect r2 = ParcelCompat.readParcelable(p, Rect.class.getClassLoader(), Rect.class);
+ assertEquals(r, r2);
+ }
+
+ @Test
+ public void readArrayInT() {
+ Parcel p = Parcel.obtain();
+
+ Signature[] s = {new Signature("1234"),
+ null,
+ new Signature("abcd")};
+ p.writeArray(s);
+
+ p.setDataPosition(0);
+ Object[] objects = ParcelCompat.readArray(p, Signature.class.getClassLoader(),
+ Signature.class);
+ assertTrue(Arrays.equals(s, objects));
+ p.setDataPosition(0);
+
+ p.recycle();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.S)
+ @Test
+ public void readSparseArrayInT() {
+ Parcel p = Parcel.obtain();
+
+ SparseArray<Signature> s = new SparseArray<>();
+ s.put(0, new Signature("1234567890abcdef"));
+ s.put(2, null);
+ s.put(3, new Signature("abcdef1234567890"));
+ p.writeSparseArray(s);
+
+ p.setDataPosition(0);
+ SparseArray<Signature> s1 = ParcelCompat.readSparseArray(p,
+ Signature.class.getClassLoader(), Signature.class);
+ assertEquals(s.size(), s1.size());
+ for (int index = 0; index < s.size(); index++) {
+ int key = s.keyAt(index);
+ assertTrue(Objects.equals(s.valueAt(index), s1.get(key)));
+ }
+
+ p.recycle();
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readListInT() {
+ Parcel p = Parcel.obtain();
+ ArrayList<Signature> s = new ArrayList();
+ ArrayList<Signature> s2 = new ArrayList();
+ s.add(new Signature("1234567890abcdef"));
+ s.add(new Signature("abcdef1234567890"));
+
+ p.writeList(s);
+ p.setDataPosition(0);
+ ParcelCompat.readList(p, s2, Signature.class.getClassLoader(), Signature.class);
+ assertEquals(2, s2.size());
+ for (int i = 0; i < s2.size(); i++) {
+ assertEquals(s.get(i), s2.get(i));
+ }
+ p.recycle();
+ }
+
+ @Test
+ public void readArrayListInT() {
+ Parcel p = Parcel.obtain();
+
+ ArrayList<Signature> s = new ArrayList<>();
+ s.add(new Signature("1234567890abcdef"));
+ s.add(null);
+ s.add(new Signature("abcdef1234567890"));
+
+ p.writeList(s);
+ p.setDataPosition(0);
+ ArrayList<Signature> s1 = ParcelCompat.readArrayList(p, Signature.class.getClassLoader(),
+ Signature.class);
+ assertEquals(s, s1);
+
+ p.recycle();
+ }
+
+ @Test
+ public void readMapInT() {
+ Parcel p = Parcel.obtain();
+ ClassLoader loader = getClass().getClassLoader();
+ HashMap<String, Signature> map = new HashMap<>();
+ HashMap<String, Signature> map2 = new HashMap<>();
+
+ map.put("key1", new Signature("abcd"));
+ map.put("key2", new Signature("ABCD"));
+ p.writeMap(map);
+ p.setDataPosition(0);
+ ParcelCompat.readMap(p, map2, Signature.class.getClassLoader(), String.class,
+ Signature.class);
+ assertEquals(map, map2);
+
+ p.recycle();
+ }
+
+ @Test
+ public void readHashMapInT() {
+ Parcel p = Parcel.obtain();
+ ClassLoader loader = getClass().getClassLoader();
+ HashMap<String, Signature> map = new HashMap<>();
+ HashMap<String, Signature> map2 = new HashMap<>();
+
+ map.put("key1", new Signature("abcd"));
+ map.put("key2", new Signature("ABCD"));
+ p.writeMap(map);
+ p.setDataPosition(0);
+ map2 = ParcelCompat.readHashMap(p, loader, String.class, Signature.class);
+ assertEquals(map, map2);
+
+ p.recycle();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ public void readParcelableCreatorInT() {
+ final String signatureString = "1234567890abcdef";
+ Signature s = new Signature(signatureString);
+
+ Parcel p = Parcel.obtain();
+ p.writeParcelableCreator(s);
+ p.setDataPosition(0);
+ assertSame(Signature.CREATOR, ParcelCompat.readParcelableCreator(p,
+ Signature.class.getClassLoader(), Signature.class));
+
+ p.setDataPosition(0);
+ p.recycle();
+ }
+
+ @Test
+ public void readParcelableArrayInT() {
+ Parcel p = Parcel.obtain();
+ Signature[] s = {new Signature("1234"),
+ null,
+ new Signature("abcd")
+ };
+ p.writeParcelableArray(s, 0);
+ p.setDataPosition(0);
+ Parcelable[] s1 = ParcelCompat.readParcelableArray(p, Signature.class.getClassLoader(),
+ Signature.class);
+ assertTrue(Arrays.equals(s, s1));
+ p.recycle();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ public void readParcelableListInT() {
+ final Parcel p = Parcel.obtain();
+ ArrayList<Signature> list = new ArrayList<>();
+ ArrayList<Signature> list1 = new ArrayList<>();
+ list.add(new Signature("1234"));
+ list.add(new Signature("4321"));
+ p.writeParcelableList(list, 0);
+ p.setDataPosition(0);
+ ParcelCompat.readParcelableList(p, list1, Signature.class.getClassLoader(),
+ Signature.class);
+ assertEquals(list, list1);
+ p.recycle();
+ }
+
+ @Test
+ public void readSerializable2Arg() {
+ String s = "Hello World";
+ Parcel p = Parcel.obtain();
+ p.writeSerializable(s);
+
+ p.setDataPosition(0);
+ String s2 = ParcelCompat.readSerializable(p, String.class.getClassLoader(), String.class);
+ assertEquals(s, s2);
+ }
}
diff --git a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
index 4499af4..5112fed 100644
--- a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
@@ -68,6 +68,7 @@
*/
@RunWith(AndroidJUnit4.class)
@MediumTest
+@SuppressWarnings("deprecation")
public class FontsContractCompatTest {
private static final String AUTHORITY = "androidx.core.provider.fonts.font";
private static final String PACKAGE = "androidx.core.test";
@@ -322,6 +323,7 @@
}
@Test
+ @SuppressWarnings("deprecation")
public void testGetProvider_duplicateCerts()
throws PackageManager.NameNotFoundException {
PackageManager packageManager = mock(PackageManager.class);
@@ -377,6 +379,7 @@
}
}
+ @SuppressWarnings("deprecation")
private ProviderInfo setupPackageManager(PackageManager packageManager)
throws PackageManager.NameNotFoundException {
ProviderInfo info = new ProviderInfo();
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index e32d604..275e727 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -125,6 +125,14 @@
assertThat(nodeCompat.isTextEntryKey(), is(false));
}
+ @Test
+ public void testGetSetUniqueId() {
+ final String uniqueId = (Build.VERSION.SDK_INT >= 19) ? "localUId" : null;
+ AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
+ nodeCompat.setUniqueId(uniqueId);
+ assertThat(nodeCompat.getUniqueId(), equalTo(uniqueId));
+ }
+
@SdkSuppress(minSdkVersion = 19)
@Test
public void testAccessibilityActionsNotNull() {
diff --git a/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
index 2a9a40b..0765571 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
@@ -489,6 +489,7 @@
@Test
@SdkSuppress(minSdkVersion = 26, maxSdkVersion = 27)
+ @SuppressWarnings("deprecation")
public void testSetCustomSelectionActionModeCallback_fixesBugInO() {
// Create mock context and package manager for the text view.
final PackageManager packageManagerMock = spy(mTextView.getContext().getPackageManager());
diff --git a/core/core/src/main/AndroidManifest.xml b/core/core/src/main/AndroidManifest.xml
index 8481017..b050c7e 100644
--- a/core/core/src/main/AndroidManifest.xml
+++ b/core/core/src/main/AndroidManifest.xml
@@ -16,4 +16,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:appComponentFactory="androidx.core.app.CoreComponentFactory" />
+ <permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
+ <uses-permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
</manifest>
diff --git a/core/core/src/main/java/androidx/core/app/ActivityCompat.java b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
index 9cd1859..48bd8fe 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
@@ -16,6 +16,7 @@
package androidx.core.app;
+import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
@@ -40,15 +41,19 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.core.content.LocusIdCompat;
+import androidx.core.os.BuildCompat;
import androidx.core.view.DragAndDropPermissionsCompat;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Helper for accessing features in {@link android.app.Activity}.
@@ -331,6 +336,7 @@
* <p>Note that this is <em>not</em> a security feature -- you can not trust the
* referrer information, applications can spoof it.</p>
*/
+ @SuppressWarnings("deprecation")
@Nullable
public static Uri getReferrer(@NonNull Activity activity) {
if (Build.VERSION.SDK_INT >= 22) {
@@ -486,6 +492,15 @@
* RuntimePermissions</a> sample app demonstrates how to use this method to
* request permissions at run time.
* </p>
+ * <p>
+ * If {@link Manifest.permission#POST_NOTIFICATIONS} is requested before the device supports
+ * the notification permission, then {@link Manifest.permission#POST_NOTIFICATIONS} will be
+ * removed from {@link OnRequestPermissionsResultCallback#onRequestPermissionsResult}.
+ * For devices that don't support {@link Manifest.permission#POST_NOTIFICATIONS}, apps can
+ * send users to its notification settings to enable notifications. See
+ * {@link android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS} for more information
+ * on launching notification settings.
+ * </p>
*
* @param activity The target activity.
* @param permissions The requested permissions. Must be non-null and not empty.
@@ -497,6 +512,7 @@
* @see #checkSelfPermission(android.content.Context, String)
* @see #shouldShowRequestPermissionRationale(android.app.Activity, String)
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) {
if (sDelegate != null
@@ -505,11 +521,32 @@
return;
}
- for (String permission : permissions) {
- if (TextUtils.isEmpty(permission)) {
+ Set<Integer> indicesOfPermissionsToRemove = new HashSet<>();
+ for (int i = 0; i < permissions.length; i++) {
+ if (TextUtils.isEmpty(permissions[i])) {
throw new IllegalArgumentException("Permission request for permissions "
+ Arrays.toString(permissions) + " must not contain null or empty values");
}
+
+ if (!BuildCompat.isAtLeastT()) {
+ if (TextUtils.equals(permissions[i], Manifest.permission.POST_NOTIFICATIONS)) {
+ indicesOfPermissionsToRemove.add(i);
+ }
+ }
+ }
+
+ int numPermissionsToRemove = indicesOfPermissionsToRemove.size();
+ final String[] permissionsArray = numPermissionsToRemove > 0
+ ? new String[permissions.length - numPermissionsToRemove] : permissions;
+ if (numPermissionsToRemove > 0) {
+ if (numPermissionsToRemove == permissions.length) {
+ return;
+ }
+ for (int i = 0, modifiedIndex = 0; i < permissions.length; i++) {
+ if (!indicesOfPermissionsToRemove.contains(i)) {
+ permissionsArray[modifiedIndex++] = permissions[i];
+ }
+ }
}
if (Build.VERSION.SDK_INT >= 23) {
@@ -523,19 +560,19 @@
handler.post(new Runnable() {
@Override
public void run() {
- final int[] grantResults = new int[permissions.length];
+ final int[] grantResults = new int[permissionsArray.length];
PackageManager packageManager = activity.getPackageManager();
String packageName = activity.getPackageName();
- final int permissionCount = permissions.length;
+ final int permissionCount = permissionsArray.length;
for (int i = 0; i < permissionCount; i++) {
grantResults[i] = packageManager.checkPermission(
- permissions[i], packageName);
+ permissionsArray[i], packageName);
}
((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
- requestCode, permissions, grantResults);
+ requestCode, permissionsArray, grantResults);
}
});
}
@@ -551,8 +588,14 @@
* @see #checkSelfPermission(Context, String)
* @see #requestPermissions(Activity, String[], int)
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
@NonNull String permission) {
+ if (!BuildCompat.isAtLeastT()
+ && TextUtils.equals(Manifest.permission.POST_NOTIFICATIONS, permission)) {
+ // notification permission doesn't exist before T
+ return false;
+ }
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.shouldShowRequestPermissionRationale(activity, permission);
}
diff --git a/core/core/src/main/java/androidx/core/app/ComponentActivity.java b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
index 764e041..206cfd2 100644
--- a/core/core/src/main/java/androidx/core/app/ComponentActivity.java
+++ b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
@@ -28,8 +28,10 @@
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.collection.SimpleArrayMap;
+import androidx.core.os.BuildCompat;
import androidx.core.view.KeyEventDispatcher;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -167,6 +169,7 @@
return !shouldSkipDump(args);
}
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
private static boolean shouldSkipDump(@Nullable String[] args) {
if (args != null && args.length > 0) {
// NOTE: values below are hardcoded on framework's Activity (like dumpInner())
@@ -177,6 +180,9 @@
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
case "--translation":
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
+ case "--list-dumpables":
+ case "--dump-dumpable":
+ return BuildCompat.isAtLeastT();
}
}
return false;
diff --git a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
index de76c7f..08343a4 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -794,6 +794,7 @@
* Check the current list of enabled listener packages and update the records map
* accordingly.
*/
+ @SuppressWarnings("deprecation")
private void updateListenerMap() {
Set<String> enabledPackages = getEnabledListenerPackages(mContext);
if (enabledPackages.equals(mCachedEnabledPackages)) {
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index c209d4f..5cfe607 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -196,7 +196,7 @@
* @param intent Intent that was launched to share content
* @return ComponentName of the calling activity
*/
- @SuppressWarnings("WeakerAccess")
+ @SuppressWarnings({"WeakerAccess", "deprecation"})
@Nullable
static ComponentName getCallingActivity(@NonNull Intent intent) {
ComponentName result = intent.getParcelableExtra(EXTRA_CALLING_ACTIVITY);
@@ -882,6 +882,7 @@
* @return A URI referring to a data stream to be shared or null if one was not supplied
* @see Intent#EXTRA_STREAM
*/
+ @SuppressWarnings("deprecation")
@Nullable
public Uri getStream() {
return mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -896,6 +897,7 @@
* @see Intent#EXTRA_STREAM
* @see Intent#ACTION_SEND_MULTIPLE
*/
+ @SuppressWarnings("deprecation")
@Nullable
public Uri getStream(int index) {
if (mStreams == null && isMultipleShare()) {
@@ -918,6 +920,7 @@
*
* @return Count of text items contained within the Intent
*/
+ @SuppressWarnings("deprecation")
public int getStreamCount() {
if (mStreams == null && isMultipleShare()) {
mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
@@ -1066,6 +1069,7 @@
*
* @return The calling application's label or null if unknown
*/
+ @SuppressWarnings("deprecation")
@Nullable
public CharSequence getCallingApplicationLabel() {
if (mCallingPackage == null) return null;
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 942112e..2ac62dc 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -84,10 +84,12 @@
import android.app.usage.UsageStatsManager;
import android.appwidget.AppWidgetManager;
import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.RestrictionsManager;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
@@ -126,6 +128,7 @@
import android.telecom.TelecomManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -139,16 +142,23 @@
import androidx.annotation.ColorRes;
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BuildCompat;
import androidx.core.os.EnvironmentCompat;
import androidx.core.os.ExecutorCompat;
import androidx.core.util.ObjectsCompat;
import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.concurrent.Executor;
@@ -190,6 +200,35 @@
return null;
}
+
+ private static final String DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX =
+ ".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
+
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef(flag = true, value = {
+ RECEIVER_VISIBLE_TO_INSTANT_APPS, RECEIVER_EXPORTED, RECEIVER_NOT_EXPORTED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RegisterReceiverFlags {}
+ /**
+ * Flag for {@link #registerReceiver}: The receiver can receive broadcasts from Instant Apps.
+ */
+ public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 0x1;
+
+ /**
+ * Flag for {@link #registerReceiver}: The receiver can receive broadcasts from other Apps.
+ * Has the same behavior as marking a statically registered receiver with "exported=true"
+ */
+ public static final int RECEIVER_EXPORTED = 0x2;
+
+ /**
+ * Flag for {@link #registerReceiver}: The receiver cannot receive broadcasts from other Apps.
+ * Has the same behavior as marking a statically registered receiver with "exported=false"
+ */
+ public static final int RECEIVER_NOT_EXPORTED = 0x4;
+
/**
* Start a set of activities as a synthesized task stack, if able.
*
@@ -550,8 +589,15 @@
* permission, or {@link PackageManager#PERMISSION_DENIED} if not.
* @see PackageManager#checkPermission(String, String)
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) {
ObjectsCompat.requireNonNull(permission, "permission must be non-null");
+ if (!BuildCompat.isAtLeastT()
+ && TextUtils.equals(android.Manifest.permission.POST_NOTIFICATIONS, permission)) {
+ return NotificationManagerCompat.from(context).areNotificationsEnabled()
+ ? PackageManager.PERMISSION_GRANTED
+ : PackageManager.PERMISSION_DENIED;
+ }
return context.checkPermission(permission, Process.myPid(), Process.myUid());
}
@@ -727,6 +773,80 @@
}
/**
+ * Register a broadcast receiver.
+ *
+ * @param context Context to retrieve service from.
+ * @param receiver The BroadcastReceiver to handle the broadcast.
+ * @param filter Selects the Intent broadcasts to be received.
+ * @param flags Specify one of {@link #RECEIVER_EXPORTED}, if you wish for your receiver
+ * to be able to receiver broadcasts from other applications, or
+ * {@link #RECEIVER_NOT_EXPORTED} if you only want your receiver to be able
+ * to receive broadcasts from the system or your own app.
+ * @return The first sticky intent found that matches <var>filter</var>,
+ * or null if there are none.
+ * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, int)
+ */
+ @Nullable
+ public static Intent registerReceiver(@NonNull Context context,
+ @Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
+ @RegisterReceiverFlags int flags) {
+ return registerReceiver(context, receiver, filter, null, null, flags);
+ }
+
+ /**
+ * Register a broadcast receiver.
+ *
+ * @param context Context to retrieve service from.
+ * @param receiver The BroadcastReceiver to handle the broadcast.
+ * @param filter Selects the Intent broadcasts to be received.
+ * @param broadcastPermission String naming a permission that a broadcaster must hold in
+ * order to send and Intent to you. If null, no permission is
+ * required.
+ * @param scheduler Handler identifying the thread will receive the Intent. If
+ * null, the main thread of the process will be used.
+ * @param flags Specify one of {@link #RECEIVER_EXPORTED}, if you wish for your
+ * receiver to be able to receiver broadcasts from other
+ * applications, or {@link #RECEIVER_NOT_EXPORTED} if you only want
+ * your receiver to be able to receive broadcasts from the system
+ * or your own app.
+ * @return The first sticky intent found that matches <var>filter</var>,
+ * or null if there are none.
+ * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int)
+ */
+ @Nullable
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public static Intent registerReceiver(@NonNull Context context,
+ @Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
+ @Nullable String broadcastPermission,
+ @Nullable Handler scheduler, @RegisterReceiverFlags int flags) {
+ if (((flags & RECEIVER_EXPORTED) == 0) && ((flags & RECEIVER_NOT_EXPORTED) == 0)) {
+ throw new IllegalArgumentException("One of either RECEIVER_EXPORTED or "
+ + "RECEIVER_NOT_EXPORTED is required");
+ }
+
+ if (((flags & RECEIVER_EXPORTED) != 0) && ((flags & RECEIVER_NOT_EXPORTED) != 0)) {
+ throw new IllegalArgumentException("Cannot specify both RECEIVER_EXPORTED and "
+ + "RECEIVER_NOT_EXPORTED");
+ }
+
+ if (BuildCompat.isAtLeastT()) {
+ return Api33Impl.registerReceiver(context, receiver, filter, broadcastPermission,
+ scheduler, flags);
+ }
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.registerReceiver(context, receiver, filter, broadcastPermission,
+ scheduler, flags);
+ }
+ if (((flags & RECEIVER_NOT_EXPORTED) != 0) && (broadcastPermission == null)) {
+ String permission =
+ context.getPackageName() + DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX;
+ return context.registerReceiver(receiver, filter, permission, scheduler /* handler */);
+ }
+ return context.registerReceiver(receiver, filter, broadcastPermission,
+ scheduler);
+ }
+
+ /**
* Gets the name of the system-level service that is represented by the specified class.
*
* @param context Context to retrieve service name from.
@@ -925,6 +1045,19 @@
// This class is not instantiable.
}
+ @DoNotInline
+ static Intent registerReceiver(Context obj, @Nullable BroadcastReceiver receiver,
+ IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) {
+ if ((flags & RECEIVER_NOT_EXPORTED) != 0 && broadcastPermission == null) {
+ String permission =
+ obj.getPackageName() + DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX;
+ // receivers that are not exported should also not be visible to instant apps
+ return obj.registerReceiver(receiver, filter, permission, scheduler);
+ }
+ flags &= Context.RECEIVER_VISIBLE_TO_INSTANT_APPS;
+ return obj.registerReceiver(receiver, filter, broadcastPermission, scheduler, flags);
+ }
+
@SuppressWarnings("UnusedReturnValue")
@DoNotInline
static ComponentName startForegroundService(Context obj, Intent service) {
@@ -955,4 +1088,17 @@
return obj.getAttributionTag();
}
}
+
+ @RequiresApi(33)
+ static class Api33Impl {
+ private Api33Impl() {
+ // This class is not instantiable
+ }
+
+ @DoNotInline
+ static Intent registerReceiver(Context obj, @Nullable BroadcastReceiver receiver,
+ IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) {
+ return obj.registerReceiver(receiver, filter, broadcastPermission, scheduler, flags);
+ }
+ }
}
diff --git a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
index 8831564..f932e1d 100644
--- a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
@@ -217,6 +217,7 @@
*/
@Nullable
@RestrictTo(LIBRARY)
+ @SuppressWarnings("deprecation")
public static String getPermissionRevocationVerifierApp(
@NonNull PackageManager packageManager) {
Intent permissionRevocationSettingsIntent =
diff --git a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
index 9a037c7..31372ce 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
@@ -81,6 +81,7 @@
* provided {@param packageManager}
*/
@NonNull
+ @SuppressWarnings("deprecation")
public static List<Signature> getSignatures(@NonNull PackageManager packageManager,
@NonNull String packageName) throws PackageManager.NameNotFoundException {
Signature[] array;
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index 33b02cb..169aba6 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -860,6 +860,7 @@
return sShortcutInfoCompatSaver;
}
+ @SuppressWarnings("deprecation")
private static List<ShortcutInfoChangeListener> getShortcutInfoListeners(Context context) {
if (sShortcutInfoChangeListeners == null) {
List<ShortcutInfoChangeListener> result = new ArrayList<>();
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
index 95a9003..5b1e2ef 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
@@ -86,6 +86,7 @@
* Returns a set of string which contains the ids of static shortcuts.
*/
@NonNull
+ @SuppressWarnings("deprecation")
private static Set<String> parseShortcutIds(@NonNull final Context context) {
final Set<String> result = new HashSet<>();
final Intent mainIntent = new Intent(Intent.ACTION_MAIN);
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index dd4b95a..8a8385c 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -682,6 +682,7 @@
return null;
}
+ @SuppressWarnings("deprecation")
static Resources getResources(Context context, String resPackage) {
if ("android".equals(resPackage)) {
return Resources.getSystem();
diff --git a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
index 3c2c8f1..47c9dca 100644
--- a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
@@ -137,6 +137,7 @@
* potentially-stale value from
* {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}.
*/
+ @SuppressWarnings("deprecation")
@SuppressLint("ReferencesDeprecated")
@Nullable
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index 988a6d5..244040a 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -200,10 +200,10 @@
* removed and all calls must be replaced with {@code Build.VERSION.SDK_INT >=
* Build.VERSION_CODES.TIRAMISU}.
*
- * @return {@code true} if T APIs are available for use, {@code false} otherwise
+ * @return {@code true} if Tiramisu APIs are available for use, {@code false} otherwise
*/
@PrereleaseSdkCheck
- @ChecksSdkIntAtLeast(codename = "Tiramisu")
+ @ChecksSdkIntAtLeast(api = 33) // 33 is wrong but required for NewApi, see b/204776549
public static boolean isAtLeastT() {
return VERSION.SDK_INT >= 32 && isAtLeastPreReleaseCodename("Tiramisu", VERSION.CODENAME);
}
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
index a09c8bf..7409305 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -23,8 +23,10 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.Size;
+import androidx.core.text.ICUCompat;
import java.util.Locale;
@@ -231,6 +233,87 @@
}
}
+ /**
+ * Determine whether two locales are considered a match, even if they are not exactly equal.
+ * They are considered as a match when both of their languages and scripts
+ * (explicit or inferred) are identical. This means that a user would be able to understand
+ * the content written in the supported locale even if they say they prefer the desired locale.
+ *
+ * E.g. [zh-HK] matches [zh-Hant]; [en-US] matches [en-CA].
+ *
+ * @param supported The supported {@link Locale} to be compared.
+ * @param desired The desired {@link Locale} to be compared.
+ * @return True if they match, false otherwise.
+ */
+ @RequiresApi(21)
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public static boolean matchesLanguageAndScript(@NonNull Locale supported,
+ @NonNull Locale desired) {
+ if (BuildCompat.isAtLeastT()) {
+ return LocaleList.matchesLanguageAndScript(supported, desired);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ return Api21Impl.matchesLanguageAndScript(supported, desired);
+ } else {
+ throw new UnsupportedOperationException(
+ "This method is only supported on API level 21+");
+ }
+ }
+
+ @RequiresApi(21)
+ static class Api21Impl {
+ private Api21Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static boolean matchesLanguageAndScript(@NonNull Locale supported,
+ @NonNull Locale desired) {
+ if (supported.equals(desired)) {
+ return true; // return early so we don't do unnecessary computation
+ }
+ if (!supported.getLanguage().equals(desired.getLanguage())) {
+ return false;
+ }
+ if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
+ // The locales are not the same, but the languages are the same, and one of the
+ // locales
+ // is a pseudo-locale. So this is not a match.
+ return false;
+ }
+ final String supportedScr = ICUCompat.maximizeAndGetScript(supported);
+ if (supportedScr.isEmpty()) {
+ // If we can't guess a script, we don't know enough about the locales' language
+ // to find
+ // if the locales match. So we fall back to old behavior of matching, which
+ // considered
+ // locales with different regions different.
+ final String supportedRegion = supported.getCountry();
+ return supportedRegion.isEmpty() || supportedRegion.equals(desired.getCountry());
+ }
+ final String desiredScr = ICUCompat.maximizeAndGetScript(desired);
+ // There is no match if the two locales use different scripts. This will most imporantly
+ // take care of traditional vs simplified Chinese.
+ return supportedScr.equals(desiredScr);
+ }
+
+ private static final Locale[] PSEUDO_LOCALE = {
+ new Locale("en", "XA"), new Locale("ar", "XB")};
+
+ private static boolean isPseudoLocale(Locale locale) {
+ for (Locale pseudoLocale : PSEUDO_LOCALE) {
+ if (pseudoLocale.equals(locale)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @DoNotInline
+ static Locale forLanguageTag(String languageTag) {
+ return Locale.forLanguageTag(languageTag);
+ }
+ }
+
@Override
public boolean equals(Object other) {
return other instanceof LocaleListCompat && mImpl.equals(((LocaleListCompat) other).mImpl);
@@ -268,16 +351,4 @@
return LocaleList.getDefault();
}
}
-
- @RequiresApi(21)
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static Locale forLanguageTag(String languageTag) {
- return Locale.forLanguageTag(languageTag);
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index a181077..8fa56eb 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -16,9 +16,23 @@
package androidx.core.os;
+import android.annotation.SuppressLint;
+import android.os.Build;
import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* Helper for accessing features in {@link Parcel}.
@@ -43,5 +57,329 @@
out.writeInt(value ? 1 : 0);
}
+ /**
+ * Same as {@link Parcel#readList(List, ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings("deprecation")
+ public static <T> void readList(@NonNull Parcel in, @NonNull List<? super T> outVal,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ TiramisuImpl.readList(in, outVal, loader, clazz);
+ } else {
+ in.readList(outVal, loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readArrayList(ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressLint({"ConcreteCollection", "NullableCollection"})
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @Nullable
+ public static <T> ArrayList<T> readArrayList(@NonNull Parcel in, @Nullable ClassLoader loader,
+ @NonNull Class<? extends T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readArrayList(in, loader, clazz);
+ } else {
+ return in.readArrayList(loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readArray(ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @SuppressLint({"ArrayReturn", "NullableCollection"})
+ @Nullable
+ public static <T> T[] readArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+ @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readArray(in, loader, clazz);
+ } else {
+ return (T[]) in.readArray(loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readSparseArray(ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings("deprecation")
+ @Nullable
+ public static <T> SparseArray<T> readSparseArray(@NonNull Parcel in,
+ @Nullable ClassLoader loader,
+ @NonNull Class<? extends T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readSparseArray(in, loader, clazz);
+ } else {
+ return in.readSparseArray(loader);
+ }
+ }
+
+
+ /**
+ * Same as {@link Parcel#readMap(Map, ClassLoader)} but accepts {@code clazzKey} and
+ * {@code clazzValue} parameter as the types required for each key and value pair.
+ *
+ * @throws android.os.BadParcelableException If the item to be deserialized is not an
+ * instance of that class or any of its children class.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings("deprecation")
+ public static <K, V> void readMap(@NonNull Parcel in, @NonNull Map<? super K, ? super V> outVal,
+ @Nullable ClassLoader loader, @NonNull Class<K> clazzKey,
+ @NonNull Class<V> clazzValue) {
+ if (BuildCompat.isAtLeastT()) {
+ TiramisuImpl.readMap(in, outVal, loader, clazzKey, clazzValue);
+ } else {
+ in.readMap(outVal, loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readHashMap(ClassLoader)} but accepts {@code clazzKey} and
+ * {@code clazzValue} parameter as the types required for each key and value pair.
+ *
+ * @throws android.os.BadParcelableException if the item to be deserialized is not an
+ * instance of that class or any of its children class.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressLint({"ConcreteCollection", "NullableCollection"})
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @Nullable
+ public static <K, V> HashMap<K, V> readHashMap(@NonNull Parcel in, @Nullable ClassLoader loader,
+ @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readHashMap(in, loader, clazzKey, clazzValue);
+ } else {
+ return in.readHashMap(loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readParcelable(ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings("deprecation")
+ @Nullable
+ public static <T extends Parcelable> T readParcelable(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readParcelable(in, loader, clazz);
+ } else {
+ return in.readParcelable(loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readParcelableCreator(ClassLoader)} but accepts {@code clazz} parameter
+ * as the required type.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there
+ * there was an error trying to read the {@link Parcelable.Creator}.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @Nullable
+ @RequiresApi(30)
+ public static <T> Parcelable.Creator<T> readParcelableCreator(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readParcelableCreator(in, loader, clazz);
+ } else {
+ return (Parcelable.Creator<T>) Api30Impl.readParcelableCreator(in, loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readParcelableArray(ClassLoader)} but accepts {@code clazz} parameter
+ * as the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @SuppressLint({"ArrayReturn", "NullableCollection"})
+ @Nullable
+ public static <T> T[] readParcelableArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+ @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readParcelableArray(in, loader, clazz);
+ } else {
+ return (T[]) in.readParcelableArray(loader);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readParcelableList(List, ClassLoader)} but accepts {@code clazz}
+ * parameter as the type required for each item.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children classes or there was
+ * an error trying to instantiate an element.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @NonNull
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ public static <T> List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
+ @Nullable ClassLoader cl, @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readParcelableList(in, list, cl, clazz);
+ } else {
+ return Api29Impl.readParcelableList(in, (List) list, cl);
+ }
+ }
+
+ /**
+ * Same as {@link Parcel#readSerializable()} but accepts {@code loader} parameter
+ * as the primary classLoader for resolving the Serializable class; and {@code clazz} parameter
+ * as the required type.
+ *
+ * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+ * deserialized is not an instance of that class or any of its children class or there there
+ * was an error deserializing the object.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings({"deprecation", "unchecked"})
+ @Nullable
+ public static <T extends Serializable> T readSerializable(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ if (BuildCompat.isAtLeastT()) {
+ return TiramisuImpl.readSerializable(in, loader, clazz);
+ } else {
+ return (T) in.readSerializable();
+ }
+ }
+
private ParcelCompat() {}
+
+ @RequiresApi(29)
+ static class Api29Impl {
+ private Api29Impl() {
+ // This class is non-instantiable.
+ }
+
+ @DoNotInline
+ static final <T extends Parcelable> List<T> readParcelableList(@NonNull Parcel in,
+ @NonNull List<T> list, @Nullable ClassLoader cl) {
+ return in.readParcelableList(list, cl);
+ }
+ }
+
+ @RequiresApi(30)
+ static class Api30Impl {
+ private Api30Impl() {
+ // This class is non-instantiable.
+ }
+
+ @DoNotInline
+ static final Parcelable.Creator<?> readParcelableCreator(@NonNull Parcel in,
+ @Nullable ClassLoader loader) {
+ return in.readParcelableCreator(loader);
+ }
+ }
+
+ @RequiresApi(33)
+ static class TiramisuImpl {
+ private TiramisuImpl() {
+ // This class is non-instantiable.
+ }
+
+ @DoNotInline
+ static <T extends Serializable> T readSerializable(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ return in.readSerializable(loader, clazz);
+ }
+
+ @DoNotInline
+ static <T extends Parcelable> T readParcelable(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ return in.readParcelable(loader, clazz);
+ }
+
+ @DoNotInline
+ public static <T> Parcelable.Creator<T> readParcelableCreator(Parcel in, ClassLoader loader,
+ Class<T> clazz) {
+ return in.readParcelableCreator(loader, clazz);
+ }
+
+ @DoNotInline
+ static <T> T[] readParcelableArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+ @NonNull Class<T> clazz) {
+ return in.readParcelableArray(loader, clazz);
+ }
+
+ @DoNotInline
+ static <T> List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
+ @Nullable ClassLoader cl, @NonNull Class<T> clazz) {
+ return in.readParcelableList(list, cl, clazz);
+ }
+
+ @DoNotInline
+ public static <T> void readList(@NonNull Parcel in, @NonNull List<? super T> outVal,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ in.readList(outVal, loader, clazz);
+ }
+
+ @DoNotInline
+ public static <T> ArrayList<T> readArrayList(Parcel in, ClassLoader loader,
+ Class<? extends T> clazz) {
+ return in.readArrayList(loader, clazz);
+ }
+
+ @DoNotInline
+ public static <T> T[] readArray(Parcel in, ClassLoader loader, Class<T> clazz) {
+ return in.readArray(loader, clazz);
+ }
+
+ @DoNotInline
+ public static <T> SparseArray<T> readSparseArray(Parcel in, ClassLoader loader,
+ Class<? extends T> clazz) {
+ return in.readSparseArray(loader, clazz);
+ }
+
+ @DoNotInline
+ public static <K, V> void readMap(Parcel in, Map<? super K, ? super V> outVal,
+ ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {
+ in.readMap(outVal, loader, clazzKey, clazzValue);
+ }
+
+ @DoNotInline
+ public static <V, K> HashMap<K, V> readHashMap(Parcel in, ClassLoader loader,
+ Class<? extends K> clazzKey, Class<? extends V> clazzValue) {
+ return in.readHashMap(loader, clazzKey, clazzValue);
+ }
+ }
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index ed6beeb..74a0ae7 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -202,6 +202,36 @@
public static final int CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 0x00000040;
/**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * A drag has started while accessibility is enabled. This is either via an
+ * AccessibilityAction, or via touch events. This is sent from the source that initiated the
+ * drag.
+ *
+ * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_START
+ */
+ public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 0x00000080;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * A drag in with accessibility enabled has ended. This means the content has been
+ * successfully dropped. This is sent from the target that accepted the dragged content.
+ *
+ * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_DROP
+ */
+ public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 0x00000100;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * A drag in with accessibility enabled has ended. This means the content has been
+ * unsuccessfully dropped, the user has canceled the action via an AccessibilityAction, or
+ * no drop has been detected within a timeout and the drag was automatically cancelled. This is
+ * sent from the source that initiated the drag.
+ *
+ * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_CANCEL
+ */
+ public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 0x0000200;
+
+ /**
* Mask for {@link AccessibilityEvent} all types.
*
* @see AccessibilityEvent#TYPE_VIEW_CLICKED
@@ -238,7 +268,10 @@
CONTENT_CHANGE_TYPE_STATE_DESCRIPTION,
CONTENT_CHANGE_TYPE_SUBTREE,
CONTENT_CHANGE_TYPE_TEXT,
- CONTENT_CHANGE_TYPE_UNDEFINED
+ CONTENT_CHANGE_TYPE_UNDEFINED,
+ CONTENT_CHANGE_TYPE_DRAG_STARTED,
+ CONTENT_CHANGE_TYPE_DRAG_DROPPED,
+ CONTENT_CHANGE_TYPE_DRAG_CANCELLED
})
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 446b6dc..2a05851 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -23,6 +23,7 @@
import static java.util.Collections.emptyList;
import android.annotation.SuppressLint;
+import android.content.ClipData;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Build;
@@ -42,6 +43,7 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.core.R;
import androidx.core.accessibilityservice.AccessibilityServiceInfoCompat;
@@ -591,6 +593,54 @@
? AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER : null,
android.R.id.accessibilityActionImeEnter, null, null, null);
+ /**
+ * Action to start a drag.
+ * <p>
+ * This action initiates a drag & drop within the system. The source's dragged content is
+ * prepared before the drag begins. In View, this action should prepare the arguments to
+ * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}} and then
+ * call the method. The equivalent should be performed for other UI toolkits.
+ * </p>
+ *
+ * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_STARTED
+ */
+ @NonNull
+ public static final AccessibilityActionCompat ACTION_DRAG_START =
+ new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START : null,
+ android.R.id.accessibilityActionDragStart, null, null, null);
+
+ /**
+ * Action to trigger a drop of the content being dragged.
+ * <p>
+ * This action is added to potential drop targets if the source started a drag with
+ * {@link #ACTION_DRAG_START}. In View, these targets are Views that accepted
+ * {@link android.view.DragEvent#ACTION_DRAG_STARTED} and have an
+ * {@link View.OnDragListener}.
+ * </p>
+ *
+ * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_DROPPED
+ */
+ @NonNull
+ public static final AccessibilityActionCompat ACTION_DRAG_DROP =
+ new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP : null,
+ android.R.id.accessibilityActionDragDrop, null, null, null);
+
+ /**
+ * Action to cancel a drag.
+ * <p>
+ * This action is added to the source that started a drag with {@link #ACTION_DRAG_START}.
+ * </p>
+ *
+ * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_CANCELLED
+ */
+ @NonNull
+ public static final AccessibilityActionCompat ACTION_DRAG_CANCEL =
+ new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL : null,
+ android.R.id.accessibilityActionDragCancel, null, null, null);
+
final Object mAction;
private final int mId;
private final Class<? extends CommandArguments> mViewCommandArgumentClass;
@@ -1235,6 +1285,9 @@
private static final String STATE_DESCRIPTION_KEY =
"androidx.view.accessibility.AccessibilityNodeInfoCompat.STATE_DESCRIPTION_KEY";
+ private static final String UNIQUE_ID_KEY =
+ "androidx.view.accessibility.AccessibilityNodeInfoCompat.UNIQUE_ID_KEY";
+
// These don't line up with the internal framework constants, since they are independent
// and we might as well get all 32 bits of utility here.
private static final int BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE = 0x00000001;
@@ -2854,6 +2907,42 @@
}
/**
+ * Gets the unique id of this node.
+ *
+ * @return the unique id or null if android version smaller
+ * than 19.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public @Nullable String getUniqueId() {
+ if (BuildCompat.isAtLeastT()) {
+ return mInfo.getUniqueId();
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ return mInfo.getExtras().getString(UNIQUE_ID_KEY);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the unique id of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param uniqueId the unique id of this node.
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void setUniqueId(@Nullable String uniqueId) {
+ if (BuildCompat.isAtLeastT()) {
+ mInfo.setUniqueId(uniqueId);
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ mInfo.getExtras().putString(UNIQUE_ID_KEY, uniqueId);
+ }
+ }
+
+ /**
* Return an instance back to be reused.
* <p>
* <strong>Note:</strong> You must not touch the object after calling this function.
@@ -4031,7 +4120,7 @@
*/
public @Nullable CharSequence getRoleDescription() {
if (Build.VERSION.SDK_INT >= 19) {
- return mInfo.getExtras().getCharSequence(ROLE_DESCRIPTION_KEY);
+ return getExtras().getCharSequence(ROLE_DESCRIPTION_KEY);
} else {
return null;
}
@@ -4063,7 +4152,7 @@
*/
public void setRoleDescription(@Nullable CharSequence roleDescription) {
if (Build.VERSION.SDK_INT >= 19) {
- mInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, roleDescription);
+ getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, roleDescription);
}
}
@@ -4169,6 +4258,7 @@
builder.append("; text: ").append(getText());
builder.append("; contentDescription: ").append(getContentDescription());
builder.append("; viewId: ").append(getViewIdResourceName());
+ builder.append("; uniqueId: ").append(getUniqueId());
builder.append("; checkable: ").append(isCheckable());
builder.append("; checked: ").append(isChecked());
@@ -4304,8 +4394,14 @@
return "ACTION_PRESS_AND_HOLD";
case android.R.id.accessibilityActionImeEnter:
return "ACTION_IME_ENTER";
+ case android.R.id.accessibilityActionDragStart:
+ return "ACTION_DRAG_START";
+ case android.R.id.accessibilityActionDragDrop:
+ return "ACTION_DRAG_DROP";
+ case android.R.id.accessibilityActionDragCancel:
+ return "ACTION_DRAG_CANCEL";
default:
- return"ACTION_UNKNOWN";
+ return "ACTION_UNKNOWN";
}
}
}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 7a6a655..dfb25e3 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -650,6 +650,7 @@
}
}
+ @SuppressWarnings("deprecation")
private List<ResolveInfo> getSupportedActivities(final Context context,
final PackageManager packageManager) {
final List<ResolveInfo> supportedActivities = new ArrayList<>();
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index fd64fdb..bd80f7b 100644
--- a/development/studio/idea.properties
+++ b/development/studio/idea.properties
@@ -5,12 +5,12 @@
#---------------------------------------------------------------------
# Uncomment this option if you want to customize path to IDE config folder. Make sure you're using forward slashes.
#---------------------------------------------------------------------
-idea.config.path=${user.home}/.AndroidStudioAndroidX/config
+idea.config.path=${user.home}/.AndroidStudioAndroidXPlatform/config
#---------------------------------------------------------------------
# Uncomment this option if you want to customize path to IDE system folder. Make sure you're using forward slashes.
#---------------------------------------------------------------------
-idea.system.path=${user.home}/.AndroidStudioAndroidX/system
+idea.system.path=${user.home}/.AndroidStudioAndroidXPlatform/system
#---------------------------------------------------------------------
# Uncomment this option if you want to customize path to user installed plugins folder. Make sure you're using forward slashes.
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index bb9f1b7..400ebac 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,16 +4,16 @@
}
dependencies {
- docs("androidx.activity:activity:1.5.0-alpha05")
- docs("androidx.activity:activity-compose:1.5.0-alpha05")
- samples("androidx.activity:activity-compose-samples:1.5.0-alpha05")
- docs("androidx.activity:activity-ktx:1.5.0-alpha05")
+ docs("androidx.activity:activity:1.6.0-alpha01")
+ docs("androidx.activity:activity-compose:1.6.0-alpha01")
+ samples("androidx.activity:activity-compose-samples:1.6.0-alpha01")
+ docs("androidx.activity:activity-ktx:1.6.0-alpha01")
docs("androidx.ads:ads-identifier:1.0.0-alpha04")
docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
docs("androidx.annotation:annotation:1.4.0-alpha02")
docs("androidx.annotation:annotation-experimental:1.2.0")
- docs("androidx.appcompat:appcompat:1.5.0-alpha01")
- docs("androidx.appcompat:appcompat-resources:1.5.0-alpha01")
+ docs("androidx.appcompat:appcompat:1.6.0-alpha01")
+ docs("androidx.appcompat:appcompat-resources:1.6.0-alpha01")
docs("androidx.appsearch:appsearch:1.0.0-alpha04")
docs("androidx.appsearch:appsearch-ktx:1.0.0-alpha04")
docs("androidx.appsearch:appsearch-local-storage:1.0.0-alpha04")
@@ -103,8 +103,8 @@
docs("androidx.core:core-role:1.1.0-rc01")
docs("androidx.core:core-animation:1.0.0-alpha02")
docs("androidx.core:core-animation-testing:1.0.0-alpha02")
- docs("androidx.core:core:1.8.0-alpha07")
- docs("androidx.core:core-ktx:1.8.0-alpha07")
+ docs("androidx.core:core:1.9.0-alpha01")
+ docs("androidx.core:core-ktx:1.9.0-alpha02")
docs("androidx.core:core-splashscreen:1.0.0-beta02")
docs("androidx.cursoradapter:cursoradapter:1.0.0")
docs("androidx.customview:customview:1.2.0-alpha01")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 925cdf1..a3d4ef5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -145,6 +145,7 @@
docs(project(":glance:glance"))
docs(project(":glance:glance-appwidget"))
docs(project(":glance:glance-wear-tiles"))
+ docs(project(":graphics:graphics-core"))
docs(project(":gridlayout:gridlayout"))
docs(project(":health:health-data-client"))
docs(project(":health:health-services-client"))
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index e5d2274..e1e449f 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -5968,7 +5968,11 @@
throw new UnsupportedOperationException("Failed to read EXIF from HEIF file. "
+ "Given stream is either malformed or unsupported.");
} finally {
- retriever.release();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
}
} else {
throw new UnsupportedOperationException("Reading EXIF from HEIF files "
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 020f2e2..ddefee4 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api("androidx.core:core-ktx:1.2.0")
+ api(projectOrArtifact(":core:core"))
api("androidx.collection:collection:1.1.0")
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
index 486e1fa..0905d7e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
@@ -82,6 +82,7 @@
*
* @see applyTrampolineIntent
*/
+@Suppress("DEPRECATION")
internal fun Activity.launchTrampolineAction(intent: Intent) {
val actionIntent = requireNotNull(intent.getParcelableExtra<Intent>(ActionIntentKey)) {
"List adapter activity trampoline invoked without specifying target intent."
diff --git a/gradle.properties b/gradle.properties
index e6166a5..30a77bb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,8 +21,11 @@
# Workaround for b/141364941
android.forceJacocoOutOfProcess=true
-# Generate versioned API files
-androidx.writeVersionedApiFiles=true
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
+
+# Don't warn about needing to update AGP
+android.suppressUnsupportedCompileSdk=Tiramisu
# Disable features we do not use
android.defaults.buildfeatures.aidl=false
diff --git a/graphics/OWNERS b/graphics/OWNERS
new file mode 100644
index 0000000..db046a2
--- /dev/null
+++ b/graphics/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1137062
+sumir@google.com
+jreck@google.com
+xxayedawgxx@google.com
+njawad@google.com
\ No newline at end of file
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
new file mode 100644
index 0000000..ec0fb93
--- /dev/null
+++ b/graphics/graphics-core/api/current.txt
@@ -0,0 +1,205 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+ public final class GLRenderer {
+ ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public boolean isRunning();
+ method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+ method public void start(optional String name);
+ method public void start();
+ method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+ method public void stop(boolean cancelPending);
+ method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+ }
+
+ public static final class GLRenderer.Companion {
+ }
+
+ public static interface GLRenderer.EglContextCallback {
+ method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+ }
+
+ public static interface GLRenderer.RenderCallback {
+ method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+ }
+
+ public static final class GLRenderer.RenderTarget {
+ method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(boolean cancelPending);
+ method public boolean isAttached();
+ method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender();
+ method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(int width, int height);
+ }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+ public final inline class EglConfigAttributes {
+ ctor public EglConfigAttributes();
+ }
+
+ public static final class EglConfigAttributes.Builder {
+ method public void include(int[] attributes);
+ method public infix void to(int, int that);
+ }
+
+ public final class EglConfigAttributesKt {
+ method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+ method public static int[] getEglConfigAttributes1010102();
+ method public static int[] getEglConfigAttributes8888();
+ method public static int[] getEglConfigAttributesF16();
+ property public static final int[] EglConfigAttributes1010102;
+ property public static final int[] EglConfigAttributes8888;
+ property public static final int[] EglConfigAttributesF16;
+ field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+ field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+ field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+ }
+
+ public final class EglException extends java.lang.RuntimeException {
+ ctor public EglException(int error, optional String msg);
+ method public int getError();
+ method public String getMsg();
+ property public final int error;
+ property public String message;
+ property public final String msg;
+ }
+
+ public final inline class EglExtensions {
+ ctor public EglExtensions();
+ }
+
+ public static final class EglExtensions.Companion {
+ method public java.util.Set<? extends java.lang.String> from(String queryString);
+ }
+
+ public final class EglExtensionsKt {
+ field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+ field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+ field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+ field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+ field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+ field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+ field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+ field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+ field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+ field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+ field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+ field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+ field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+ field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+ }
+
+ public final class EglManager {
+ ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+ method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface getCurrentDrawSurface();
+ method public android.opengl.EGLSurface getCurrentReadSurface();
+ method public android.opengl.EGLSurface getDefaultSurface();
+ method public android.opengl.EGLConfig? getEglConfig();
+ method public android.opengl.EGLContext? getEglContext();
+ method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+ method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+ method public void initialize();
+ method public boolean isExtensionSupported(String extensionName);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+ method public void release();
+ method public void swapAndFlushBuffers();
+ property public final android.opengl.EGLSurface currentDrawSurface;
+ property public final android.opengl.EGLSurface currentReadSurface;
+ property public final android.opengl.EGLSurface defaultSurface;
+ property public final android.opengl.EGLConfig? eglConfig;
+ property public final android.opengl.EGLContext? eglContext;
+ property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+ property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+ field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+ }
+
+ public static final class EglManager.Companion {
+ }
+
+ public interface EglSpec {
+ method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+ method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+ method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+ method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+ method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+ method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+ method public int eglGetError();
+ method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+ method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+ method public String eglQueryString(int nameId);
+ method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+ method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+ method public default String getErrorMessage();
+ method public default static String getStatusString(int error);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+ }
+
+ public static final class EglSpec.Companion {
+ method public String getStatusString(int error);
+ }
+
+ public final class EglVersion {
+ ctor public EglVersion(int major, int minor);
+ method public int component1();
+ method public int component2();
+ method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+ method public int getMajor();
+ method public int getMinor();
+ property public final int major;
+ property public final int minor;
+ field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+ field public static final androidx.graphics.opengl.egl.EglVersion V14;
+ field public static final androidx.graphics.opengl.egl.EglVersion V15;
+ }
+
+ public static final class EglVersion.Companion {
+ }
+
+}
+
+package androidx.graphics.surface {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+ ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+ method protected void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Companion {
+ }
+
+ public static class SurfaceControlCompat.Transaction {
+ ctor public SurfaceControlCompat.Transaction();
+ method public final void delete();
+ method public final void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Transaction.Companion {
+ }
+
+}
+
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..ec0fb93
--- /dev/null
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -0,0 +1,205 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+ public final class GLRenderer {
+ ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public boolean isRunning();
+ method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+ method public void start(optional String name);
+ method public void start();
+ method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+ method public void stop(boolean cancelPending);
+ method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+ }
+
+ public static final class GLRenderer.Companion {
+ }
+
+ public static interface GLRenderer.EglContextCallback {
+ method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+ }
+
+ public static interface GLRenderer.RenderCallback {
+ method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+ }
+
+ public static final class GLRenderer.RenderTarget {
+ method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(boolean cancelPending);
+ method public boolean isAttached();
+ method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender();
+ method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(int width, int height);
+ }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+ public final inline class EglConfigAttributes {
+ ctor public EglConfigAttributes();
+ }
+
+ public static final class EglConfigAttributes.Builder {
+ method public void include(int[] attributes);
+ method public infix void to(int, int that);
+ }
+
+ public final class EglConfigAttributesKt {
+ method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+ method public static int[] getEglConfigAttributes1010102();
+ method public static int[] getEglConfigAttributes8888();
+ method public static int[] getEglConfigAttributesF16();
+ property public static final int[] EglConfigAttributes1010102;
+ property public static final int[] EglConfigAttributes8888;
+ property public static final int[] EglConfigAttributesF16;
+ field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+ field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+ field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+ }
+
+ public final class EglException extends java.lang.RuntimeException {
+ ctor public EglException(int error, optional String msg);
+ method public int getError();
+ method public String getMsg();
+ property public final int error;
+ property public String message;
+ property public final String msg;
+ }
+
+ public final inline class EglExtensions {
+ ctor public EglExtensions();
+ }
+
+ public static final class EglExtensions.Companion {
+ method public java.util.Set<? extends java.lang.String> from(String queryString);
+ }
+
+ public final class EglExtensionsKt {
+ field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+ field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+ field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+ field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+ field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+ field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+ field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+ field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+ field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+ field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+ field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+ field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+ field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+ field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+ }
+
+ public final class EglManager {
+ ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+ method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface getCurrentDrawSurface();
+ method public android.opengl.EGLSurface getCurrentReadSurface();
+ method public android.opengl.EGLSurface getDefaultSurface();
+ method public android.opengl.EGLConfig? getEglConfig();
+ method public android.opengl.EGLContext? getEglContext();
+ method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+ method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+ method public void initialize();
+ method public boolean isExtensionSupported(String extensionName);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+ method public void release();
+ method public void swapAndFlushBuffers();
+ property public final android.opengl.EGLSurface currentDrawSurface;
+ property public final android.opengl.EGLSurface currentReadSurface;
+ property public final android.opengl.EGLSurface defaultSurface;
+ property public final android.opengl.EGLConfig? eglConfig;
+ property public final android.opengl.EGLContext? eglContext;
+ property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+ property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+ field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+ }
+
+ public static final class EglManager.Companion {
+ }
+
+ public interface EglSpec {
+ method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+ method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+ method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+ method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+ method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+ method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+ method public int eglGetError();
+ method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+ method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+ method public String eglQueryString(int nameId);
+ method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+ method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+ method public default String getErrorMessage();
+ method public default static String getStatusString(int error);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+ }
+
+ public static final class EglSpec.Companion {
+ method public String getStatusString(int error);
+ }
+
+ public final class EglVersion {
+ ctor public EglVersion(int major, int minor);
+ method public int component1();
+ method public int component2();
+ method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+ method public int getMajor();
+ method public int getMinor();
+ property public final int major;
+ property public final int minor;
+ field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+ field public static final androidx.graphics.opengl.egl.EglVersion V14;
+ field public static final androidx.graphics.opengl.egl.EglVersion V15;
+ }
+
+ public static final class EglVersion.Companion {
+ }
+
+}
+
+package androidx.graphics.surface {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+ ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+ method protected void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Companion {
+ }
+
+ public static class SurfaceControlCompat.Transaction {
+ ctor public SurfaceControlCompat.Transaction();
+ method public final void delete();
+ method public final void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Transaction.Companion {
+ }
+
+}
+
diff --git a/graphics/graphics-core/api/res-current.txt b/graphics/graphics-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/graphics/graphics-core/api/res-current.txt
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
new file mode 100644
index 0000000..4dce33b
--- /dev/null
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -0,0 +1,207 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+ public final class GLRenderer {
+ ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public boolean isRunning();
+ method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+ method public void start(optional String name);
+ method public void start();
+ method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+ method public void stop(boolean cancelPending);
+ method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+ field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+ }
+
+ public static final class GLRenderer.Companion {
+ }
+
+ public static interface GLRenderer.EglContextCallback {
+ method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+ }
+
+ public static interface GLRenderer.RenderCallback {
+ method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+ method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+ }
+
+ public static final class GLRenderer.RenderTarget {
+ method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+ method public void detach(boolean cancelPending);
+ method public boolean isAttached();
+ method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+ method public void requestRender();
+ method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+ method public void resize(int width, int height);
+ }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+ public final inline class EglConfigAttributes {
+ ctor public EglConfigAttributes();
+ }
+
+ public static final class EglConfigAttributes.Builder {
+ ctor @kotlin.PublishedApi internal EglConfigAttributes.Builder();
+ method @kotlin.PublishedApi internal int[] build();
+ method public void include(int[] attributes);
+ method public infix void to(int, int that);
+ }
+
+ public final class EglConfigAttributesKt {
+ method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+ method public static int[] getEglConfigAttributes1010102();
+ method public static int[] getEglConfigAttributes8888();
+ method public static int[] getEglConfigAttributesF16();
+ property public static final int[] EglConfigAttributes1010102;
+ property public static final int[] EglConfigAttributes8888;
+ property public static final int[] EglConfigAttributesF16;
+ field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+ field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+ field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+ }
+
+ public final class EglException extends java.lang.RuntimeException {
+ ctor public EglException(int error, optional String msg);
+ method public int getError();
+ method public String getMsg();
+ property public final int error;
+ property public String message;
+ property public final String msg;
+ }
+
+ public final inline class EglExtensions {
+ ctor public EglExtensions();
+ }
+
+ public static final class EglExtensions.Companion {
+ method public java.util.Set<? extends java.lang.String> from(String queryString);
+ }
+
+ public final class EglExtensionsKt {
+ field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+ field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+ field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+ field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+ field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+ field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+ field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+ field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+ field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+ field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+ field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+ field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+ field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+ field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+ }
+
+ public final class EglManager {
+ ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+ method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface getCurrentDrawSurface();
+ method public android.opengl.EGLSurface getCurrentReadSurface();
+ method public android.opengl.EGLSurface getDefaultSurface();
+ method public android.opengl.EGLConfig? getEglConfig();
+ method public android.opengl.EGLContext? getEglContext();
+ method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+ method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+ method public void initialize();
+ method public boolean isExtensionSupported(String extensionName);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+ method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+ method public void release();
+ method public void swapAndFlushBuffers();
+ property public final android.opengl.EGLSurface currentDrawSurface;
+ property public final android.opengl.EGLSurface currentReadSurface;
+ property public final android.opengl.EGLSurface defaultSurface;
+ property public final android.opengl.EGLConfig? eglConfig;
+ property public final android.opengl.EGLContext? eglContext;
+ property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+ property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+ field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+ }
+
+ public static final class EglManager.Companion {
+ }
+
+ public interface EglSpec {
+ method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+ method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+ method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+ method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+ method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+ method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+ method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+ method public int eglGetError();
+ method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+ method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+ method public String eglQueryString(int nameId);
+ method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+ method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+ method public default String getErrorMessage();
+ method public default static String getStatusString(int error);
+ method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+ field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+ }
+
+ public static final class EglSpec.Companion {
+ method public String getStatusString(int error);
+ }
+
+ public final class EglVersion {
+ ctor public EglVersion(int major, int minor);
+ method public int component1();
+ method public int component2();
+ method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+ method public int getMajor();
+ method public int getMinor();
+ property public final int major;
+ property public final int minor;
+ field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+ field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+ field public static final androidx.graphics.opengl.egl.EglVersion V14;
+ field public static final androidx.graphics.opengl.egl.EglVersion V15;
+ }
+
+ public static final class EglVersion.Companion {
+ }
+
+}
+
+package androidx.graphics.surface {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+ ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+ method protected void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Companion {
+ }
+
+ public static class SurfaceControlCompat.Transaction {
+ ctor public SurfaceControlCompat.Transaction();
+ method public final void delete();
+ method public final void finalize();
+ field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+ }
+
+ public static final class SurfaceControlCompat.Transaction.Companion {
+ }
+
+}
+
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
new file mode 100644
index 0000000..c421162
--- /dev/null
+++ b/graphics/graphics-core/build.gradle
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ implementation 'androidx.annotation:annotation:1.2.0'
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 17
+ externalNativeBuild {
+ cmake {
+ cppFlags ''
+ }
+ }
+ }
+ ndkVersion "23.1.7779620"
+ externalNativeBuild {
+ cmake {
+ path file('src/main/cpp/CMakeLists.txt')
+ version '3.22.1'
+ }
+ }
+}
+
+androidx {
+ name = "Android Graphics Core"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.GRAPHICS
+ inceptionYear = "2021"
+ description = "Leverage graphics facilities across multiple Android platform releases"
+}
diff --git a/graphics/graphics-core/lint-baseline.xml b/graphics/graphics-core/lint-baseline.xml
new file mode 100644
index 0000000..5bcb65c
--- /dev/null
+++ b/graphics/graphics-core/lint-baseline.xml
@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 7.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta02)" variant="all" version="7.1.0-beta02">
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testConfig8888() {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+ line="30"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testConfig1010102() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+ line="47"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testConfigF16() {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+ line="63"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testInclude() {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+ line="80"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testSupportsBufferAge() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="28"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testSupportBufferAgeFromPartialUpdate() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="33"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testSetDamage() {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="41"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testSwapBuffersWithDamage() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="49"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testColorSpace() {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="57"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testNoConfigContext() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="65"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testPixelFormatFloat() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="73"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testScRgb() {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="81"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testDisplayP3() {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="89"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testHDR() {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="97"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testContextPriority() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="105"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testSurfacelessContext() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="113"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testFenceSync() {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="121"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testWaitSync() {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="128"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testNativeFenceSync() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="133"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testExtensionsQueryStringParsing() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+ line="141"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testDestructuringComponents() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+ line="28"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testEquals() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+ line="35"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testToString() {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+ line="40"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun testHashCode() {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+ line="45"
+ column="9"/>
+ </issue>
+
+</issues>
diff --git a/graphics/graphics-core/src/androidTest/AndroidManifest.xml b/graphics/graphics-core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..141b3c9
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.graphics.core.test">
+
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+ <supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
+ <supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
+ <application>
+ <activity android:name="androidx.graphics.opengl.egl.EglTestActivity"
+ android:label="Graphics Core Test"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="androidx.graphics.opengl.GLTestActivity"
+ android:label="Graphics Core Test"
+ android:exported="true">
+ </activity>
+ </application>
+</manifest>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
new file mode 100644
index 0000000..e2a0a96
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -0,0 +1,702 @@
+/*
+ * Copyright 2022 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 androidx.graphics.opengl
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.media.Image
+import android.media.ImageReader
+import android.opengl.EGL14
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.view.PixelCopy
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import androidx.lifecycle.Lifecycle.State
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GLRendererTest {
+
+ @Test
+ fun testStartAfterStop() {
+ with(GLRenderer()) {
+ start("thread1")
+ stop(true)
+ start("thread2")
+ stop(true)
+ }
+ }
+
+ @Test
+ fun testAttachBeforeStartThrows() {
+ try {
+ with(GLRenderer()) {
+ attach(
+ Surface(SurfaceTexture(17)),
+ 10,
+ 10,
+ object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ // NO-OP
+ }
+ })
+ }
+ fail("Start should be called first")
+ } catch (exception: IllegalStateException) {
+ // Success, attach before call to start should fail
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testRender() {
+ val latch = CountDownLatch(1)
+ val renderer = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val width = 5
+ val height = 8
+ val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target = glRenderer.attach(reader.surface, width, height, renderer)
+ target.requestRender {
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ val plane = reader.acquireLatestImage().planes[0]
+ assertEquals(4, plane.pixelStride)
+
+ val targetColor = Color.argb(255, 255, 0, 255)
+ Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+ target.detach(true)
+
+ glRenderer.stop(true)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testDetachExecutesPendingRequests() {
+ val latch = CountDownLatch(1)
+ val renderer = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val width = 5
+ val height = 8
+ val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target = glRenderer.attach(reader.surface, width, height, renderer)
+ target.requestRender {
+ latch.countDown()
+ }
+ target.detach(false) // RequestRender Call should still execute
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ val plane = reader.acquireLatestImage().planes[0]
+ assertEquals(4, plane.pixelStride)
+
+ val targetColor = Color.argb(255, 255, 0, 255)
+ Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+ glRenderer.stop(true)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testStopExecutesPendingRequests() {
+ val latch = CountDownLatch(1)
+ val surfaceWidth = 5
+ val surfaceHeight = 8
+ val renderer = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+ assertEquals(surfaceWidth, size.width)
+ assertEquals(surfaceHeight, size.height)
+ GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val reader = ImageReader.newInstance(surfaceWidth, surfaceHeight, PixelFormat.RGBA_8888, 1)
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target = glRenderer.attach(reader.surface, surfaceWidth, surfaceHeight, renderer)
+ target.requestRender {
+ latch.countDown()
+ }
+ glRenderer.stop(false) // RequestRender call should still execute
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ val plane = reader.acquireLatestImage().planes[0]
+ assertEquals(4, plane.pixelStride)
+
+ val targetColor = Color.argb(255, 255, 0, 255)
+ Api19Helpers.verifyPlaneContent(surfaceWidth, surfaceHeight, plane, targetColor)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testDetachExecutesMultiplePendingRequests() {
+ val numRenders = 4
+ val latch = CountDownLatch(numRenders)
+ val renderCount = AtomicInteger(0)
+ val renderer = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ var red: Float = 0f
+ var green: Float = 0f
+ var blue: Float = 0f
+ when (renderCount.get()) {
+ 1 -> {
+ red = 1f
+ }
+ 2 -> {
+ green = 1f
+ }
+ 3 -> {
+ blue = 1f
+ }
+ }
+ GLES20.glClearColor(red, green, blue, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val width = 5
+ val height = 8
+ val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target = glRenderer.attach(reader.surface, width, height, renderer)
+ // Issuing multiple requestRender calls to ensure each of them are
+ // executed even when a detach call is made
+ repeat(numRenders) {
+ target.requestRender {
+ renderCount.incrementAndGet()
+ latch.countDown()
+ }
+ }
+
+ target.detach(false) // RequestRender calls should still execute
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(numRenders, renderCount.get())
+ val plane = reader.acquireLatestImage().planes[0]
+ assertEquals(4, plane.pixelStride)
+
+ val targetColor = Color.argb(255, 0, 0, 255)
+ Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+ glRenderer.stop(true)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testDetachCancelsPendingRequests() {
+ val latch = CountDownLatch(1)
+ val renderer = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val width = 5
+ val height = 8
+ val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target = glRenderer.attach(reader.surface, width, height, renderer)
+ target.requestRender {
+ latch.countDown()
+ }
+ target.detach(false) // RequestRender Call should be cancelled
+
+ glRenderer.stop(true)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ fun testMultipleAttachedSurfaces() {
+ val latch = CountDownLatch(2)
+ val renderer1 = object : GLRenderer.RenderCallback {
+
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val renderer2 = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ val width1 = 6
+ val height1 = 7
+
+ val width2 = 11
+ val height2 = 23
+ val reader1 = ImageReader.newInstance(width1, height1, PixelFormat.RGBA_8888, 1)
+
+ val reader2 = ImageReader.newInstance(width2, height2, PixelFormat.RGBA_8888, 1)
+
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+
+ val target1 = glRenderer.attach(reader1.surface, width1, height1, renderer1)
+ val target2 = glRenderer.attach(reader2.surface, width2, height2, renderer2)
+ target1.requestRender {
+ latch.countDown()
+ }
+ target2.requestRender {
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ val plane1 = reader1.acquireLatestImage().planes[0]
+ val plane2 = reader2.acquireLatestImage().planes[0]
+
+ Api19Helpers.verifyPlaneContent(width1, height1, plane1, Color.argb(255, 255, 0, 0))
+ Api19Helpers.verifyPlaneContent(width2, height2, plane2, Color.argb(255, 0, 0, 255))
+
+ target1.detach(true)
+ target2.detach(true)
+
+ val attachLatch = CountDownLatch(1)
+ glRenderer.stop(true) {
+ attachLatch.countDown()
+ }
+
+ assertTrue(attachLatch.await(3000, TimeUnit.MILLISECONDS))
+ }
+
+ /**
+ * Helper class for test methods that refer to APIs that may not exist on earlier API levels.
+ * This must be broken out into a separate class instead of being defined within the
+ * test class as the test runner will inspect all methods + parameter types in advance.
+ * If a parameter type does not exist on a particular API level, it will crash even if
+ * there are corresponding @SdkSuppress and @RequiresApi
+ * See https://b.corp.google.com/issues/221485597
+ */
+ class Api19Helpers private constructor() {
+ companion object {
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ fun verifyPlaneContent(width: Int, height: Int, plane: Image.Plane, targetColor: Int) {
+ val rowPadding = plane.rowStride - plane.pixelStride * width
+ var offset = 0
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val red = plane.buffer[offset].toInt() and 0xff
+ val green = plane.buffer[offset + 1].toInt() and 0xff
+ val blue = plane.buffer[offset + 2].toInt() and 0xff
+ val alpha = plane.buffer[offset + 3].toInt() and 0xff
+ val packedColor = Color.argb(alpha, red, green, blue)
+ assertEquals("Index: $x, $y", targetColor, packedColor)
+ offset += plane.pixelStride
+ }
+ offset += rowPadding
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testNonStartedGLRendererIsNotRunning() {
+ assertFalse(GLRenderer().isRunning())
+ }
+
+ @Test
+ fun testRepeatedStartAndStopRunningState() {
+ val glRenderer = GLRenderer()
+ assertFalse(glRenderer.isRunning())
+ glRenderer.start()
+ assertTrue(glRenderer.isRunning())
+ glRenderer.stop(true)
+ assertFalse(glRenderer.isRunning())
+ glRenderer.start()
+ assertTrue(glRenderer.isRunning())
+ glRenderer.stop(true)
+ assertFalse(glRenderer.isRunning())
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ fun testSurfaceViewAttach() {
+ withGLTestActivity {
+ assertNotNull(surfaceView)
+
+ val latch = CountDownLatch(1)
+ val glRenderer = GLRenderer().apply { start() }
+ val target = glRenderer.attach(surfaceView, ColorRenderCallback(Color.BLUE))
+
+ target.requestRender {
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+
+ val bitmap = Bitmap.createBitmap(
+ GLTestActivity.TARGET_WIDTH,
+ GLTestActivity.TARGET_HEIGHT,
+ Bitmap.Config.ARGB_8888
+ )
+
+ blockingPixelCopy(bitmap) { surfaceView.holder.surface }
+
+ assertTrue(bitmap.isAllColor(Color.BLUE))
+
+ val stopLatch = CountDownLatch(1)
+ glRenderer.stop(true) {
+ stopLatch.countDown()
+ }
+
+ assertTrue(stopLatch.await(3000, TimeUnit.MILLISECONDS))
+ // Assert that targets are detached when the GLRenderer is stopped
+ assertFalse(target.isAttached())
+ }
+ }
+
+ @Test
+ fun testTextureViewOnResizeCalled() {
+ withGLTestActivity {
+ assertNotNull(textureView)
+ val glRenderer = GLRenderer().apply { start() }
+
+ val resizeLatch = CountDownLatch(1)
+ val target = glRenderer.attach(textureView, object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+ assertTrue(size.width > 0)
+ assertTrue(size.height > 0)
+ resizeLatch.countDown()
+ }
+ })
+ target.requestRender()
+
+ assertTrue(resizeLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val detachLatch = CountDownLatch(1)
+ target.detach(false) {
+ detachLatch.countDown()
+ }
+ assertTrue(detachLatch.await(3000, TimeUnit.MILLISECONDS))
+ glRenderer.stop(true)
+ }
+ }
+
+ @Test
+ fun testSurfaceViewOnResizeCalled() {
+ withGLTestActivity {
+ assertNotNull(surfaceView)
+ val glRenderer = GLRenderer().apply { start() }
+
+ val resizeLatch = CountDownLatch(1)
+ val target = glRenderer.attach(surfaceView, object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+ assertTrue(size.width > 0)
+ assertTrue(size.height > 0)
+ resizeLatch.countDown()
+ }
+ })
+ target.requestRender()
+
+ assertTrue(resizeLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val detachLatch = CountDownLatch(1)
+ target.detach(false) {
+ detachLatch.countDown()
+ }
+ assertTrue(detachLatch.await(3000, TimeUnit.MILLISECONDS))
+ glRenderer.stop(true)
+ }
+ }
+
+ data class Size(val width: Int, val height: Int)
+
+ fun EglSpec.querySurfaceSize(eglSurface: EGLSurface): Size {
+ val result = IntArray(1)
+ eglQuerySurface(
+ eglSurface, EGL14.EGL_WIDTH, result, 0)
+ val width = result[0]
+ eglQuerySurface(
+ eglSurface, EGL14.EGL_HEIGHT, result, 0)
+ val height = result[0]
+ return Size(width, height)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ fun testTextureViewAttach() {
+ withGLTestActivity {
+ assertNotNull(textureView)
+
+ val latch = CountDownLatch(1)
+ val glRenderer = GLRenderer().apply { start() }
+ val target = glRenderer.attach(textureView, ColorRenderCallback(Color.BLUE))
+ target.requestRender {
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+
+ val bitmap = Bitmap.createBitmap(
+ GLTestActivity.TARGET_WIDTH,
+ GLTestActivity.TARGET_HEIGHT,
+ Bitmap.Config.ARGB_8888
+ )
+
+ blockingPixelCopy(bitmap) { Surface(textureView.surfaceTexture) }
+ assertTrue(bitmap.isAllColor(Color.BLUE))
+
+ val stopLatch = CountDownLatch(1)
+ glRenderer.stop(true) {
+ stopLatch.countDown()
+ }
+
+ assertTrue(stopLatch.await(3000, TimeUnit.MILLISECONDS))
+ // Assert that targets are detached when the GLRenderer is stopped
+ assertFalse(target.isAttached())
+ }
+ }
+
+ @Test
+ fun testEglContextCallbackInvoked() {
+ val createdLatch = CountDownLatch(1)
+ val destroyedLatch = CountDownLatch(1)
+ val createCount = AtomicInteger()
+ val destroyCount = AtomicInteger()
+ val callback = object : GLRenderer.EglContextCallback {
+
+ override fun onEglContextCreated(eglManager: EglManager) {
+ createCount.incrementAndGet()
+ createdLatch.countDown()
+ }
+
+ override fun onEglContextDestroyed(eglManager: EglManager) {
+ destroyCount.incrementAndGet()
+ destroyedLatch.countDown()
+ }
+ }
+
+ val glRenderer = GLRenderer().apply { start() }
+ glRenderer.registerEglContextCallback(callback)
+
+ glRenderer.attach(
+ Surface(SurfaceTexture(12)),
+ 10,
+ 10,
+ ColorRenderCallback(Color.RED)
+ ).requestRender()
+
+ assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(1, createCount.get())
+
+ glRenderer.stop(true)
+
+ assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(1, destroyCount.get())
+ }
+
+ @Test
+ fun testEglContextCallbackInvokedBeforeStart() {
+ val createdLatch = CountDownLatch(1)
+ val destroyedLatch = CountDownLatch(1)
+ val createCount = AtomicInteger()
+ val destroyCount = AtomicInteger()
+ val callback = object : GLRenderer.EglContextCallback {
+
+ override fun onEglContextCreated(eglManager: EglManager) {
+ createCount.incrementAndGet()
+ createdLatch.countDown()
+ }
+
+ override fun onEglContextDestroyed(eglManager: EglManager) {
+ destroyCount.incrementAndGet()
+ destroyedLatch.countDown()
+ }
+ }
+
+ val glRenderer = GLRenderer()
+ // Adding a callback before the glRenderer is started should still
+ // deliver onEglRendererCreated callbacks
+ glRenderer.registerEglContextCallback(callback)
+ glRenderer.start()
+
+ glRenderer.attach(
+ Surface(SurfaceTexture(12)),
+ 10,
+ 10,
+ ColorRenderCallback(Color.CYAN)
+ ).requestRender()
+
+ assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(1, createCount.get())
+
+ glRenderer.stop(true)
+
+ assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(1, destroyCount.get())
+ }
+
+ @Test
+ fun testEglContextCallbackRemove() {
+ val createdLatch = CountDownLatch(1)
+ val destroyedLatch = CountDownLatch(1)
+ val createCount = AtomicInteger()
+ val destroyCount = AtomicInteger()
+ val callback = object : GLRenderer.EglContextCallback {
+
+ override fun onEglContextCreated(eglManager: EglManager) {
+ createCount.incrementAndGet()
+ createdLatch.countDown()
+ }
+
+ override fun onEglContextDestroyed(eglManager: EglManager) {
+ destroyCount.incrementAndGet()
+ }
+ }
+
+ val glRenderer = GLRenderer()
+ // Adding a callback before the glRenderer is started should still
+ // deliver onEglRendererCreated callbacks
+ glRenderer.registerEglContextCallback(callback)
+ glRenderer.start()
+
+ glRenderer.attach(
+ Surface(SurfaceTexture(12)),
+ 10,
+ 10,
+ ColorRenderCallback(Color.CYAN)
+ ).requestRender()
+
+ assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(1, createCount.get())
+
+ glRenderer.unregisterEglContextCallback(callback)
+
+ glRenderer.stop(false) {
+ destroyedLatch.countDown()
+ }
+
+ assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertEquals(0, destroyCount.get())
+ }
+
+ /**
+ * Helper method to create a GLTestActivity instance and progress it through the Activity
+ * lifecycle to the resumed state so we can issue rendering commands into the corresponding
+ * SurfaceView/TextureView
+ */
+ private fun withGLTestActivity(block: GLTestActivity.() -> Unit) {
+ ActivityScenario.launch(GLTestActivity::class.java).moveToState(State.RESUMED).onActivity {
+ block(it!!)
+ }
+ }
+
+ /**
+ * Helper RenderCallback that renders a solid color and invokes the provided CountdownLatch
+ * when rendering is complete
+ */
+ private class ColorRenderCallback(
+ val targetColor: Int
+ ) : GLRenderer.RenderCallback {
+
+ override fun onDrawFrame(eglManager: EglManager) {
+ GLES20.glClearColor(
+ Color.red(targetColor) / 255f,
+ Color.green(targetColor) / 255f,
+ Color.blue(targetColor) / 255f,
+ Color.alpha(targetColor) / 255f,
+ )
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ /**
+ * Helper method that synchronously blocks until the PixelCopy operation is complete
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun blockingPixelCopy(
+ destBitmap: Bitmap,
+ surfaceProvider: () -> Surface
+ ) {
+ val copyLatch = CountDownLatch(1)
+ val copyThread = HandlerThread("copyThread").apply { start() }
+ val copyHandler = Handler(copyThread.looper)
+ PixelCopy.request(surfaceProvider.invoke(),
+ destBitmap,
+ { copyResult ->
+ assertEquals(PixelCopy.SUCCESS, copyResult)
+ copyLatch.countDown()
+ copyThread.quit()
+ },
+ copyHandler
+ )
+ assertTrue(copyLatch.await(3000, TimeUnit.MILLISECONDS))
+ }
+
+ private fun Bitmap.isAllColor(targetColor: Int): Boolean {
+ for (i in 0 until width) {
+ for (j in 0 until height) {
+ if (getPixel(i, j) != targetColor) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt
new file mode 100644
index 0000000..103c36b
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 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 androidx.graphics.opengl
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.SurfaceView
+import android.view.TextureView
+import android.widget.LinearLayout
+
+class GLTestActivity : Activity() {
+
+ companion object {
+ const val TARGET_WIDTH = 30
+ const val TARGET_HEIGHT = 20
+ }
+
+ lateinit var surfaceView: SurfaceView
+ lateinit var textureView: TextureView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ surfaceView = SurfaceView(this)
+ textureView = TextureView(this)
+ val ll = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ weightSum = 2f
+ }
+ val layoutParams = LinearLayout.LayoutParams(TARGET_WIDTH, TARGET_HEIGHT)
+
+ ll.addView(surfaceView, layoutParams)
+ ll.addView(textureView, layoutParams)
+
+ setContentView(ll)
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt
new file mode 100644
index 0000000..5a5ea46
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.opengl.EGL14
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglConfigAttributesTest {
+
+ @Test
+ fun testConfig8888() {
+ with(EglConfigAttributes8888.attrs) {
+ assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+ assertTrue(find(EGL14.EGL_RED_SIZE, 8))
+ assertTrue(find(EGL14.EGL_GREEN_SIZE, 8))
+ assertTrue(find(EGL14.EGL_BLUE_SIZE, 8))
+ assertTrue(find(EGL14.EGL_ALPHA_SIZE, 8))
+ assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+ assertTrue(find(EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE))
+ assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+ assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+ assertEquals(this[size - 1], EGL14.EGL_NONE)
+ assertEquals(19, size)
+ }
+ }
+
+ @Test
+ fun testConfig1010102() {
+ with(EglConfigAttributes1010102.attrs) {
+ assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+ assertTrue(find(EGL14.EGL_RED_SIZE, 10))
+ assertTrue(find(EGL14.EGL_GREEN_SIZE, 10))
+ assertTrue(find(EGL14.EGL_BLUE_SIZE, 10))
+ assertTrue(find(EGL14.EGL_ALPHA_SIZE, 2))
+ assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+ assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+ assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+ assertEquals(this[size - 1], EGL14.EGL_NONE)
+ assertEquals(17, size)
+ }
+ }
+
+ @Test
+ fun testConfigF16() {
+ with(EglConfigAttributesF16.attrs) {
+ assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+ assertTrue(find(EglColorComponentTypeExt, EglColorComponentTypeFloatExt))
+ assertTrue(find(EGL14.EGL_RED_SIZE, 16))
+ assertTrue(find(EGL14.EGL_GREEN_SIZE, 16))
+ assertTrue(find(EGL14.EGL_BLUE_SIZE, 16))
+ assertTrue(find(EGL14.EGL_ALPHA_SIZE, 16))
+ assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+ assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+ assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+ assertEquals(this[size - 1], EGL14.EGL_NONE)
+ assertEquals(19, size)
+ }
+ }
+
+ @Test
+ fun testInclude() {
+ // Verify that custom config that uses an include initially and overwrites
+ // individual values is handled appropriately even if the config is technically invalid
+ val customConfig = EglConfigAttributes {
+ include(EglConfigAttributes8888)
+ EGL14.EGL_RED_SIZE to 27
+ EglColorComponentTypeExt to EglColorComponentTypeFloatExt
+ EGL14.EGL_STENCIL_SIZE to 32
+ }
+
+ with(customConfig.attrs) {
+ assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+ assertTrue(find(EGL14.EGL_RED_SIZE, 27))
+ assertTrue(find(EglColorComponentTypeExt, EglColorComponentTypeFloatExt))
+ assertTrue(find(EGL14.EGL_STENCIL_SIZE, 32))
+ assertEquals(this[size - 1], EGL14.EGL_NONE)
+ assertEquals(21, size)
+ }
+ }
+
+ private fun IntArray.find(key: Int, value: Int): Boolean {
+ // size - 1 to skip trailing EGL_NONE
+ for (i in 0 until this.size - 1 step 2) {
+ if (this[i] == key) {
+ return this[i + 1] == value
+ }
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt
new file mode 100644
index 0000000..7396807
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglExtensionsTest {
+
+ @Test
+ fun testSupportsBufferAge() {
+ assertTrue(EglExtensions(setOf("EGL_EXT_buffer_age")).isExtensionSupported(EglExtBufferAge))
+ }
+
+ @Test
+ fun testSupportBufferAgeFromPartialUpdate() {
+ // Buffer age can be supported from either EGL_EXT_buffer_age or EGL_KHR_partial_update
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_partial_update")).isExtensionSupported(EglKhrPartialUpdate)
+ )
+ }
+
+ @Test
+ fun testSetDamage() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_partial_update"))
+ .isExtensionSupported(EglKhrPartialUpdate)
+ )
+ }
+
+ @Test
+ fun testSwapBuffersWithDamage() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_swap_buffers_with_damage"))
+ .isExtensionSupported(EglKhrSwapBuffersWithDamage)
+ )
+ }
+
+ @Test
+ fun testColorSpace() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_gl_colorspace"))
+ .isExtensionSupported(EglKhrGlColorSpace)
+ )
+ }
+
+ @Test
+ fun testNoConfigContext() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_no_config_context"))
+ .isExtensionSupported(EglKhrNoConfigContext)
+ )
+ }
+
+ @Test
+ fun testPixelFormatFloat() {
+ assertTrue(
+ EglExtensions(setOf("EGL_EXT_pixel_format_float"))
+ .isExtensionSupported(EglExtPixelFormatFloat)
+ )
+ }
+
+ @Test
+ fun testScRgb() {
+ assertTrue(
+ EglExtensions(setOf("EGL_EXT_gl_colorspace_scrgb"))
+ .isExtensionSupported(EglExtGlColorSpaceScRgb)
+ )
+ }
+
+ @Test
+ fun testDisplayP3() {
+ assertTrue(
+ EglExtensions(setOf("EGL_EXT_gl_colorspace_display_p3_passthrough"))
+ .isExtensionSupported(EglExtColorSpaceDisplayP3Passthrough)
+ )
+ }
+
+ @Test
+ fun testHDR() {
+ assertTrue(
+ EglExtensions(setOf("EGL_EXT_gl_colorspace_bt2020_pq"))
+ .isExtensionSupported(EglExtGlColorSpaceBt2020Pq)
+ )
+ }
+
+ @Test
+ fun testContextPriority() {
+ assertTrue(
+ EglExtensions(setOf("EGL_IMG_context_priority"))
+ .isExtensionSupported(EglImgContextPriority)
+ )
+ }
+
+ @Test
+ fun testSurfacelessContext() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_surfaceless_context"))
+ .isExtensionSupported(EglKhrSurfacelessContext)
+ )
+ }
+
+ @Test
+ fun testFenceSync() {
+ assertTrue(
+ EglExtensions(setOf("EGL_KHR_fence_sync")).isExtensionSupported(EglKhrFenceSync)
+ )
+ }
+
+ @Test
+ fun testWaitSync() {
+ assertTrue(EglExtensions(setOf("EGL_KHR_wait_sync")).isExtensionSupported(EglKhrWaitSync))
+ }
+
+ @Test
+ fun testNativeFenceSync() {
+ assertTrue(
+ EglExtensions(setOf("EGL_ANDROID_native_fence_sync"))
+ .isExtensionSupported(EglAndroidNativeFenceSync)
+ )
+ }
+
+ @Test
+ fun testExtensionsQueryStringParsing() {
+ val extensionQuery = "EGL_EXT_buffer_age " +
+ "EGL_KHR_partial_update " +
+ "EGL_KHR_swap_buffers_with_damage " +
+ "EGL_KHR_gl_colorspace " +
+ "EGL_KHR_no_config_context " +
+ "EGL_EXT_pixel_format_float " +
+ "EGL_EXT_gl_colorspace_scrgb " +
+ "EGL_EXT_gl_colorspace_display_p3_passthrough " +
+ "EGL_EXT_gl_colorspace_bt2020_pq " +
+ "EGL_IMG_context_priority " +
+ "EGL_KHR_surfaceless_context " +
+ "EGL_KHR_fence_sync " +
+ "EGL_KHR_wait_sync " +
+ "EGL_ANDROID_native_fence_sync "
+ with(EglExtensions.from(extensionQuery)) {
+ assertTrue(isExtensionSupported(EglExtBufferAge))
+ assertTrue(isExtensionSupported(EglKhrPartialUpdate))
+ assertTrue(isExtensionSupported(EglKhrSwapBuffersWithDamage))
+ assertTrue(isExtensionSupported(EglKhrGlColorSpace))
+ assertTrue(isExtensionSupported(EglKhrNoConfigContext))
+ assertTrue(isExtensionSupported(EglExtPixelFormatFloat))
+ assertTrue(isExtensionSupported(EglExtGlColorSpaceScRgb))
+ assertTrue(isExtensionSupported(EglExtColorSpaceDisplayP3Passthrough))
+ assertTrue(isExtensionSupported(EglExtGlColorSpaceBt2020Pq))
+ assertTrue(isExtensionSupported(EglImgContextPriority))
+ assertTrue(isExtensionSupported(EglKhrSurfacelessContext))
+ assertTrue(isExtensionSupported(EglKhrFenceSync))
+ assertTrue(isExtensionSupported(EglKhrWaitSync))
+ assertTrue(isExtensionSupported(EglAndroidNativeFenceSync))
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt
new file mode 100644
index 0000000..e2ff28a6
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.media.ImageReader
+import android.opengl.EGL14
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.concurrent.thread
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EglManagerTest {
+
+ @Test
+ fun testInitializeAndRelease() {
+ testEglManager {
+ initialize()
+ val config = loadConfig(EglConfigAttributes8888)?.also {
+ createContext(it)
+ }
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+ // Even though EGL v 1.5 was introduced in API level 29 not all devices will advertise
+ // support for it. However, all devices should at least support EGL v 1.4
+ assertTrue(
+ "Unexpected EGL version, received $eglVersion",
+ eglVersion == EglVersion.V14 || eglVersion == EglVersion.V15
+ )
+ assertNotNull(eglContext)
+ assertNotNull(eglConfig)
+ }
+ }
+
+ @Test
+ fun testMultipleInitializeCallsIgnored() {
+ testEglManager {
+ initialize()
+ loadConfig(EglConfigAttributes8888)?.also {
+ createContext(it)
+ }
+ val currentContext = eglContext
+ val currentConfig = eglConfig
+ assertNotEquals(EGL14.EGL_NO_CONTEXT, currentContext)
+ // Subsequent calls to initialize should be ignored
+ // and the current EglContext should be the same as the previous call
+ initialize()
+ assertTrue(currentContext === eglContext)
+ assertTrue(currentConfig === eglConfig)
+ }
+ }
+
+ @Test
+ fun testMultipleReleaseCallsIgnored() {
+ testEglManager {
+ initialize()
+ loadConfig(EglConfigAttributes8888)?.also {
+ createContext(it)
+ }
+ // Multiple attempts to release should act as no-ops, i.e. we should not crash
+ // and the corresponding context should be nulled out
+ release()
+ assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+
+ release()
+ assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+ }
+ }
+
+ @Test
+ fun testDefaultSurface() {
+ testEglManager {
+ initialize()
+
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+ val config = loadConfig(EglConfigAttributes8888)
+
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+
+ createContext(config!!)
+
+ if (isExtensionSupported(EglKhrSurfacelessContext)) {
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ } else {
+ assertNotEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ }
+
+ assertEquals(currentDrawSurface, defaultSurface)
+ assertEquals(currentReadSurface, defaultSurface)
+
+ release()
+
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+ }
+ }
+
+ @Test
+ fun testDefaultSurfaceWithoutSurfacelessContext() {
+ // Create a new EGL Spec instance that does not support the
+ // EglKhrSurfacelessContext extension in order to verify
+ // the fallback support of initializing the current surface
+ // to a PBuffer instead of EGL14.EGL_NO_SURFACE
+ val wrappedEglSpec = object : EglSpec by EglSpec.Egl14 {
+ override fun eglQueryString(nameId: Int): String {
+ val queryString = EglSpec.Egl14.eglQueryString(nameId)
+ return if (nameId == EGL14.EGL_EXTENSIONS) {
+ // Parse the space separated string of EGL extensions into a set
+ val set = HashSet<String>().apply {
+ addAll(queryString.split(' '))
+ }
+ // Remove EglKhrSurfacelessContext if it exists
+ // and repack the set into a space separated string
+ set.remove(EglKhrSurfacelessContext)
+ StringBuilder().let {
+ for (entry in set) {
+ it.append(entry)
+ it.append(' ')
+ }
+ it.toString()
+ }
+ } else {
+ queryString
+ }
+ }
+ }
+
+ testEglManager(wrappedEglSpec) {
+ initialize()
+
+ // Verify that the wrapped EGL spec implementation in fact does not
+ // advertise support for EglKhrSurfacelessContext
+ assertFalse(isExtensionSupported(EglKhrSurfacelessContext))
+
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+ val config = loadConfig(EglConfigAttributes8888)
+
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+
+ // Create context at this point should fallback of eglCreatePBufferSurface
+ // instead of EGL_NO_SURFACE as a result of no longer advertising support
+ // for EglKhrSurfacelessContext
+ createContext(config!!)
+
+ assertNotEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, defaultSurface)
+ assertEquals(currentReadSurface, defaultSurface)
+
+ release()
+
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+ }
+ }
+
+ @Test
+ fun testCreatePBufferSurface() {
+ testEglManager {
+ initialize()
+
+ assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+ assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+ val config = loadConfig(EglConfigAttributes8888)
+
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+ createContext(config!!)
+
+ val pBuffer = eglSpec.eglCreatePBufferSurface(
+ config,
+ EglConfigAttributes {
+ EGL14.EGL_WIDTH to 1
+ EGL14.EGL_HEIGHT to 1
+ })
+
+ makeCurrent(pBuffer)
+
+ assertNotEquals(EGL14.EGL_NO_SURFACE, currentReadSurface)
+ assertNotEquals(EGL14.EGL_NO_SURFACE, currentDrawSurface)
+ assertNotEquals(EGL14.EGL_NO_SURFACE, pBuffer)
+
+ assertEquals(pBuffer, currentReadSurface)
+ assertEquals(pBuffer, currentDrawSurface)
+
+ eglSpec.eglDestroySurface(pBuffer)
+ release()
+ }
+ }
+
+ @Test
+ fun testCreateWindowSurfaceDefault() {
+ testEglManager {
+ initialize()
+
+ val config = loadConfig(EglConfigAttributes8888)
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+
+ createContext(config!!)
+
+ val surface = Surface(SurfaceTexture(42))
+ // Create a window surface with the default attributes
+ val eglSurface = eglSpec.eglCreateWindowSurface(config, surface, null)
+ assertNotEquals(EGL14.EGL_NO_SURFACE, eglSurface)
+ eglSpec.eglDestroySurface(eglSurface)
+
+ release()
+ }
+ }
+
+ private fun EglSpec.isSingleBufferedSurface(surface: EGLSurface): Boolean {
+ return if (surface == EGL14.EGL_NO_SURFACE) {
+ false
+ } else {
+ val result = IntArray(1)
+ val queryResult = eglQuerySurface(
+ surface, EGL14.EGL_RENDER_BUFFER, result, 0)
+ queryResult && result[0] == EGL14.EGL_SINGLE_BUFFER
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ @Test
+ fun testSurfaceContentsWithBackBuffer() {
+ verifySurfaceContentsWithWindowConfig()
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ @Test
+ fun testSurfaceContentsWithFrontBuffer() {
+ verifySurfaceContentsWithWindowConfig(true)
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ private fun verifySurfaceContentsWithWindowConfig(
+ singleBuffered: Boolean = false
+ ) {
+ testEglManager {
+ initialize()
+ val config = loadConfig(EglConfigAttributes8888)
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+ createContext(config!!)
+
+ val width = 8
+ val height = 5
+ val targetColor = Color.RED
+ val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+ var canRender = false
+
+ thread {
+ canRender = drawSurface(imageReader.surface, targetColor, singleBuffered)
+ }.join()
+
+ try {
+ if (canRender) {
+ val image = imageReader.acquireLatestImage()
+ val plane = image.planes[0]
+ assertEquals(4, plane.pixelStride)
+
+ val pixelStride = plane.pixelStride
+ val rowStride = plane.rowStride
+ val rowPadding = rowStride - pixelStride * width
+ var offset = 0
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val red = plane.buffer[offset].toInt() and 0xff
+ val green = plane.buffer[offset + 1].toInt() and 0xff
+ val blue = plane.buffer[offset + 2].toInt() and 0xff
+ val alpha = plane.buffer[offset + 3].toInt() and 0xff
+ val packedColor = Color.argb(alpha, red, green, blue)
+ assertEquals("Index: " + x + ", " + y, targetColor, packedColor)
+ offset += pixelStride
+ }
+ offset += rowPadding
+ }
+ }
+ } finally {
+ imageReader.close()
+ release()
+ }
+ }
+ }
+
+ private fun drawSurface(
+ surface: Surface,
+ color: Int,
+ singleBuffered: Boolean
+ ): Boolean {
+ var canRender = false
+ testEglManager {
+ initialize()
+ val config = loadConfig(EglConfigAttributes8888)
+ if (config == null) {
+ fail("Config 8888 should be supported")
+ }
+ createContext(config!!)
+ val configAttributes = if (singleBuffered) {
+ EglConfigAttributes {
+ EGL14.EGL_RENDER_BUFFER to EGL14.EGL_SINGLE_BUFFER
+ }
+ } else {
+ null
+ }
+ val eglSurface = eglSpec.eglCreateWindowSurface(config, surface, configAttributes)
+ // Skip tests of the device does not support EGL_SINGLE_BUFFER
+ canRender = !singleBuffered || eglSpec.isSingleBufferedSurface(eglSurface)
+ if (canRender) {
+ makeCurrent(eglSurface)
+ assertEquals("Make current failed", EGL14.EGL_SUCCESS, eglSpec.eglGetError())
+ GLES20.glClearColor(
+ Color.red(color) / 255f,
+ Color.green(color) / 255f,
+ Color.blue(color) / 255f,
+ Color.alpha(color) / 255f
+ )
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ swapAndFlushBuffers()
+ assertEquals("Swapbuffers failed", EGL14.EGL_SUCCESS, eglSpec.eglGetError())
+ }
+
+ eglSpec.eglDestroySurface(eglSurface)
+ release()
+ }
+ return canRender
+ }
+
+ /**
+ * Helper method to ensure EglManager has the corresponding release calls
+ * made to it and verifies that no exceptions were thrown as part of the test.
+ */
+ private fun testEglManager(
+ eglSpec: EglSpec = EglSpec.Egl14,
+ block: EglManager.() -> Unit = {}
+ ) {
+ with(EglManager(eglSpec)) {
+ assertEquals(EglVersion.Unknown, eglVersion)
+ assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+ block()
+ release()
+ assertEquals(EglVersion.Unknown, eglVersion)
+ assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt
new file mode 100644
index 0000000..35c4355
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.animation.ValueAnimator
+import android.app.Activity
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Bundle
+import android.view.Surface
+import android.view.SurfaceView
+import android.view.TextureView
+import android.widget.LinearLayout
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.GLRenderer.RenderTarget
+import java.util.concurrent.atomic.AtomicInteger
+
+const val TAG: String = "EGLTestActivity"
+
+class EglTestActivity : Activity() {
+
+ private val mGLRenderer = GLRenderer()
+ private val mParam = AtomicInteger()
+ private val mRenderer1 = object : GLRenderer.RenderCallback {
+ override fun onSurfaceCreated(
+ spec: EglSpec,
+ config: EGLConfig,
+ surface: Surface,
+ width: Int,
+ height: Int
+ ): EGLSurface {
+ val attrs = EglConfigAttributes {
+ EGL14.EGL_RENDER_BUFFER to EGL14.EGL_SINGLE_BUFFER
+ }
+ return spec.eglCreateWindowSurface(config, surface, attrs)
+ }
+
+ override fun onDrawFrame(eglManager: EglManager) {
+ val red = mParam.toFloat() / 100f
+ GLES20.glClearColor(red, 0.0f, 0.0f, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ private val mRenderer2 = object : GLRenderer.RenderCallback {
+ override fun onDrawFrame(eglManager: EglManager) {
+ val blue = mParam.toFloat() / 100f
+ GLES20.glClearColor(0.0f, 0.0f, blue, 1.0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
+ }
+
+ private lateinit var mSurfaceView: SurfaceView
+ private lateinit var mTextureView: TextureView
+ private lateinit var mRenderTarget1: RenderTarget
+ private lateinit var mRenderTarget2: RenderTarget
+
+ private var mAnimator: ValueAnimator? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mGLRenderer.start()
+
+ mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+ duration = 3000
+ repeatCount = ValueAnimator.INFINITE
+ repeatMode = ValueAnimator.REVERSE
+ addUpdateListener {
+ mParam.set(((it.animatedValue as Float) * 100).toInt())
+ mRenderTarget1.requestRender()
+ mRenderTarget2.requestRender()
+ }
+ start()
+ }
+
+ val container = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ weightSum = 2f
+ }
+ mSurfaceView = SurfaceView(this)
+ mTextureView = TextureView(this)
+
+ val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0).apply {
+ weight = 1f
+ }
+
+ mRenderTarget1 = mGLRenderer.attach(mSurfaceView, mRenderer1)
+ mRenderTarget2 = mGLRenderer.attach(mTextureView, mRenderer2)
+
+ container.addView(mSurfaceView, params)
+ container.addView(mTextureView, params)
+
+ setContentView(container)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!mRenderTarget1.isAttached()) {
+ mRenderTarget1 = mGLRenderer.attach(mSurfaceView, mRenderer1)
+ }
+
+ if (!mRenderTarget2.isAttached()) {
+ mRenderTarget2 = mGLRenderer.attach(mTextureView, mRenderer2)
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mRenderTarget1.detach(true)
+ mRenderTarget2.detach(true)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mAnimator?.cancel()
+ mGLRenderer.stop(true)
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt
new file mode 100644
index 0000000..87e2208
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglVersionTest {
+
+ @Test
+ fun testDestructuringComponents() {
+ val (major, minor) = EglVersion(8, 3)
+ assertEquals(8, major)
+ assertEquals(3, minor)
+ }
+
+ @Test
+ fun testEquals() {
+ assertEquals(EglVersion(2, 9), EglVersion(2, 9))
+ }
+
+ @Test
+ fun testToString() {
+ assertEquals("EGL version 5.9", EglVersion(5, 9).toString())
+ }
+
+ @Test
+ fun testHashCode() {
+ val hashCode = 31 * 8 + 4
+ assertEquals(hashCode, EglVersion(8, 4).hashCode())
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
new file mode 100644
index 0000000..ccb26b8
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 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 androidx.graphics.surface
+
+import android.os.Build
+import android.view.Surface
+import android.view.SurfaceControl
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(Build.VERSION_CODES.Q)
+@SdkSuppress(minSdkVersion = 29)
+class SurfaceControlCompatTest {
+
+ @Test
+ fun testCreateFromWindow() {
+ var surfaceControl = SurfaceControl.Builder()
+ .setName("SurfaceControlCompact_createFromWindow")
+ .build()
+ try {
+ SurfaceControlCompat(Surface(surfaceControl), "SurfaceControlCompatTest")
+ } catch (e: IllegalArgumentException) {
+ fail()
+ }
+ }
+
+ @Test
+ fun testSurfaceTransactionCreate() {
+ try {
+ SurfaceControlCompat.Transaction()
+ } catch (e: java.lang.IllegalArgumentException) {
+ fail()
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/AndroidManifest.xml b/graphics/graphics-core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0082b69
--- /dev/null
+++ b/graphics/graphics-core/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.graphics.core">
+
+</manifest>
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md b/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md
new file mode 100644
index 0000000..ffaf9e0
--- /dev/null
+++ b/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md
@@ -0,0 +1,5 @@
+# Module root
+
+AndroidX Graphics Core
+
+# Support classes for building applications that leverage more advanced graphics facilities
diff --git a/graphics/graphics-core/src/main/cpp/CMakeLists.txt b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..6349ccf
--- /dev/null
+++ b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,53 @@
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.18.1)
+
+# Declares and names the project.
+
+project("graphics-core")
+
+add_definitions(-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+add_library( # Sets the name of the library.
+ graphics-core
+
+ # Sets the library as a shared library.
+ SHARED
+
+ # Provides a relative path to your source file(s).
+ graphics-core.cpp )
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log )
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+ graphics-core
+
+ # Links the target library to the log library
+ # included in the NDK.
+ ${log-lib}
+
+ android)
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
new file mode 100644
index 0000000..6255927
--- /dev/null
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright 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.
+ */
+#include <jni.h>
+#include <string>
+#include <android/native_activity.h>
+#include <android/surface_control.h>
+#include <android/api-level.h>
+#include <android/native_window_jni.h>
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_nCreateFromWindow(JNIEnv *env, jobject thiz,
+ jobject surface,
+ jstring debug_name) {
+ if (android_get_device_api_level() >= 29) {
+ auto AWindow = ANativeWindow_fromSurface(env, surface);
+ auto debugName = env->GetStringUTFChars(debug_name, nullptr);
+ auto surfaceControl = reinterpret_cast<jlong>(ASurfaceControl_createFromWindow(AWindow,
+ debugName));
+
+ ANativeWindow_release(AWindow);
+ return surfaceControl;
+ } else {
+ return 0;
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_nRelease(JNIEnv *env, jobject thiz,
+ jlong surfaceControl) {
+ if (android_get_device_api_level() >= 29) {
+ ASurfaceControl_release(reinterpret_cast<ASurfaceControl *>(surfaceControl));
+ } else {
+ return;
+ }
+}
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_00024Transaction_nTransactionCreate(
+ JNIEnv *env, jobject thiz) {
+ if (android_get_device_api_level() >= 29) {
+ return reinterpret_cast<jlong>(ASurfaceTransaction_create());
+ } else {
+ return 0;
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_00024Transaction_nTransactionDelete(
+ JNIEnv *env, jobject thiz,
+ jlong surfaceTransaction) {
+ if (android_get_device_api_level() >= 29) {
+ ASurfaceTransaction_delete(reinterpret_cast<ASurfaceTransaction *>(surfaceTransaction));
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
new file mode 100644
index 0000000..f150a79
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -0,0 +1,681 @@
+/*
+ * Copyright 2022 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 androidx.graphics.opengl
+
+import android.graphics.SurfaceTexture
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.egl.EglConfigAttributes8888
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Class responsible for coordination of requests to render into surfaces using OpenGL.
+ * This creates a backing thread to handle EGL dependencies and draw leveraging OpenGL across
+ * multiple [android.view.Surface] instances that can be attached and detached throughout
+ * the lifecycle of an application. Usage of this class is recommended to be done on the UI thread.
+ *
+ * @param eglSpecFactory Callback invoked to determine the EGL spec version to use
+ * for EGL management. This is invoked on the GL Thread
+ * @param eglConfigFactory Callback invoked to determine the appropriate EGLConfig used
+ * to create the EGL context. This is invoked on the GL Thread
+ */
+// GL is the industry standard for referencing OpenGL vs Gl (lowercase l)
+@Suppress("AcronymName")
+class GLRenderer(
+ eglSpecFactory: () -> EglSpec = { EglSpec.Egl14 },
+ eglConfigFactory: EglManager.() -> EGLConfig = {
+ // 8 bit channels should always be supported
+ loadConfig(EglConfigAttributes8888)
+ ?: throw IllegalStateException("Unable to obtain config for 8 bit EGL " +
+ "configuration")
+ }
+) {
+
+ /**
+ * Factory method to determine which EglSpec the underlying EglManager implementation uses
+ */
+ private val mEglSpecFactory: () -> EglSpec = eglSpecFactory
+
+ /**
+ * Factory method used to create the corresponding EGLConfig used to create the EGLRenderer used
+ * by EglManager
+ */
+ private val mEglConfigFactory: EglManager.() -> EGLConfig = eglConfigFactory
+
+ /**
+ * GLThread used to manage EGL dependencies, create EGLSurfaces and draw content
+ */
+ private var mGLThread: GLThread? = null
+
+ /**
+ * Collection of [RenderTarget] instances that are managed by the GLRenderer
+ */
+ private val mRenderTargets = ArrayList<RenderTarget>()
+
+ /**
+ * Collection of callbacks to be invoked when the EGL dependencies are initialized
+ * or torn down
+ */
+ private val mEglContextCallback = HashSet<EglContextCallback>()
+
+ /**
+ * Removes the corresponding [RenderTarget] from management of the GLThread.
+ * This destroys the EGLSurface associated with this surface and subsequent requests
+ * to render into the surface with the provided token are ignored.
+ *
+ * If the [cancelPending] flag is set to true, any queued request
+ * to render that has not started yet is cancelled. However, if this is invoked in the
+ * middle of the frame being rendered, it will continue to process the current frame.
+ *
+ * Additionally if this flag is false, all pending requests to render will be processed
+ * before the [RenderTarget] is detached.
+ *
+ * Note the detach operation will only occur if the GLRenderer is started, that is if
+ * [isRunning] returns true. Otherwise this is a no-op. GLRenderer will automatically detach all
+ * [RenderTarget] instances as part of its teardown process.
+ */
+ @JvmOverloads
+ fun detach(
+ target: RenderTarget,
+ cancelPending: Boolean,
+ @WorkerThread onDetachComplete: ((RenderTarget) -> Unit)? = null
+ ) {
+ if (mRenderTargets.contains(target)) {
+ mGLThread?.detachSurface(target.token, cancelPending) {
+ // WorkerThread
+ target.release()
+ target.onDetach.invoke()
+ onDetachComplete?.invoke(target)
+ }
+ mRenderTargets.remove(target)
+ }
+ }
+
+ /**
+ * Determines if the GLThread has been started. That is [start] has been invoked
+ * on this GLRenderer instance without a corresponding call to [stop].
+ */
+ fun isRunning(): Boolean = mGLThread != null
+
+ /**
+ * Starts the GLThread. After this method is called, consumers can attempt
+ * to attach [android.view.Surface] instances through [attach] as well as
+ * schedule content to be drawn through [requestRender]
+ *
+ * @param name Optional name to provide to the GLThread
+ *
+ * @throws IllegalStateException if EGLConfig with desired attributes cannot be created
+ */
+ @JvmOverloads
+ fun start(
+ name: String = "GLThread",
+ ) {
+ if (mGLThread == null) {
+ GLThread.log("starting thread...")
+ mGLThread = GLThread(
+ name,
+ mEglSpecFactory,
+ mEglConfigFactory
+ ).apply {
+ start()
+ if (!mEglContextCallback.isEmpty()) {
+ // Add a copy of the current collection as new entries to mEglContextCallback
+ // could be mistakenly added multiple times.
+ this.addEglCallbacks(ArrayList<EglContextCallback>(mEglContextCallback))
+ }
+ }
+ }
+ }
+
+ /**
+ * Mark the corresponding surface session with the given token as dirty
+ * to schedule a call to [RenderCallback#onDrawFrame].
+ * If there is already a queued request to render into the provided surface with
+ * the specified token, this request is ignored.
+ *
+ * Note the render operation will only occur if the GLRenderer is started, that is if
+ * [isRunning] returns true. Otherwise this is a no-op.
+ *
+ * @param target RenderTarget to be re-rendered
+ * @param onRenderComplete Optional callback invoked on the backing thread after the frame has
+ * been rendered.
+ */
+ @JvmOverloads
+ fun requestRender(target: RenderTarget, onRenderComplete: ((RenderTarget) -> Unit)? = null) {
+ val token = target.token
+ val callbackRunnable = if (onRenderComplete != null) {
+ Runnable {
+ onRenderComplete.invoke(target)
+ }
+ } else {
+ null
+ }
+ mGLThread?.requestRender(token, callbackRunnable)
+ }
+
+ /**
+ * Resize the corresponding surface associated with the RenderTarget to the specified
+ * width and height and re-render. This will destroy the EGLSurface created by
+ * [RenderCallback.onSurfaceCreated] and invoke it again with the updated dimensions.
+ * An optional callback is invoked on the backing thread after the resize operation
+ * is complete.
+ *
+ * Note the resize operation will only occur if the GLRenderer is started, that is if
+ * [isRunning] returns true. Otherwise this is a no-op.
+ *
+ * @param target RenderTarget to be resized
+ * @param width Updated width of the corresponding surface
+ * @param height Updated height of the corresponding surface
+ * @param onResizeComplete Optional callback invoked on the backing thread when the resize
+ * operation is complete
+ */
+ @JvmOverloads
+ fun resize(
+ target: RenderTarget,
+ width: Int,
+ height: Int,
+ onResizeComplete: ((RenderTarget) -> Unit)? = null
+ ) {
+ val token = target.token
+ val callbackRunnable = if (onResizeComplete != null) {
+ Runnable {
+ onResizeComplete.invoke(target)
+ }
+ } else {
+ null
+ }
+ mGLThread?.resizeSurface(token, width, height, callbackRunnable)
+ }
+
+ /**
+ * Stop the corresponding GL thread. This destroys all EGLSurfaces as well
+ * as any other EGL dependencies. All queued requests that have not been processed
+ * yet are cancelled.
+ *
+ * Note the stop operation will only occur if the GLRenderer was previously started, that is
+ * [isRunning] returns true. Otherwise this is a no-op.
+ *
+ * @param cancelPending If true all pending requests and cancelled and the backing thread is
+ * torn down immediately. If false, all pending requests are processed first before tearing
+ * down the backing thread. Subsequent requests made after this call are ignored.
+ * @param onStop Optional callback invoked on the backing thread after it is torn down.
+ */
+ @JvmOverloads
+ fun stop(cancelPending: Boolean, onStop: ((GLRenderer) -> Unit)? = null) {
+ GLThread.log("stopping thread...")
+ // Make a copy of the render targets to call cleanup operations on to avoid potential
+ // concurrency issues.
+ // This method will clear the existing collection and we do not want to potentially tear
+ // down a target that was attached after a subsequent call to start if the tear down
+ // callback execution is delayed if previously pending requests have not been cancelled
+ // (i.e. cancelPending is false)
+ val renderTargets = ArrayList(mRenderTargets)
+ mGLThread?.tearDown(cancelPending) {
+ // No need to call target.detach as this callback is invoked after
+ // the dependencies are cleaned up
+ for (target in renderTargets) {
+ target.release()
+ target.onDetach.invoke()
+ }
+ onStop?.invoke(this@GLRenderer)
+ }
+ mGLThread = null
+ mRenderTargets.clear()
+ }
+
+ /**
+ * Add an [EglContextCallback] to receive callbacks for construction and
+ * destruction of EGL dependencies.
+ *
+ * These callbacks are invoked on the backing thread.
+ */
+ fun registerEglContextCallback(callback: EglContextCallback) {
+ mEglContextCallback.add(callback)
+ mGLThread?.addEglCallback(callback)
+ }
+
+ /**
+ * Remove [EglContextCallback] to no longer receive callbacks for construction and
+ * destruction of EGL dependencies.
+ *
+ * These callbacks are invoked on the backing thread
+ */
+ fun unregisterEglContextCallback(callback: EglContextCallback) {
+ mEglContextCallback.remove(callback)
+ mGLThread?.removeEglCallback(callback)
+ }
+
+ /**
+ * Callbacks invoked when the GL dependencies are created and destroyed.
+ * These are logical places to setup and tear down any dependencies that are used
+ * for drawing content within a frame (ex. compiling shaders)
+ */
+ interface EglContextCallback {
+
+ /**
+ * Callback invoked on the backing thread after EGL dependencies are initialized.
+ * This is guaranteed to be invoked before any instance of
+ * [RenderCallback.onSurfaceCreated] is called.
+ * This will be invoked lazily before the first request to [GLRenderer.requestRender]
+ */
+ @WorkerThread
+ fun onEglContextCreated(eglManager: EglManager)
+
+ /**
+ * Callback invoked on the backing thread before EGL dependencies are about to be torn down.
+ * This is invoked after [GLRenderer.stop] is processed.
+ */
+ @WorkerThread
+ fun onEglContextDestroyed(eglManager: EglManager)
+ }
+
+ /**
+ * Interface used for creating an [EGLSurface] with a user defined configuration
+ * from the provided surface as well as a callback used to render content into the surface
+ * for a given frame
+ */
+ interface RenderCallback {
+ /**
+ * Used to create a corresponding [EGLSurface] from the provided
+ * [android.view.Surface] instance. This enables consumers to configure
+ * the corresponding [EGLSurface] they wish to render into.
+ * The [EGLSurface] created here is guaranteed to be the current surface
+ * before [onDrawFrame] is called. That is, implementations of onDrawFrame
+ * do not need to call eglMakeCurrent on this [EGLSurface].
+ *
+ * This method is invoked on the GL thread.
+ *
+ * The default implementation will create a window surface with EGL_WIDTH and EGL_HEIGHT
+ * set to [width] and [height] respectively.
+ * Implementations can override this method to provide additional EglConfigAttributes
+ * for this surface (ex. [EGL14.EGL_SINGLE_BUFFER]
+ *
+ * @param spec EGLSpec used to create the corresponding EGLSurface
+ * @param config EGLConfig used to create the corresponding EGLSurface
+ * @param surface [android.view.Surface] used to create an EGLSurface from
+ * @param width Desired width of the surface to create
+ * @param height Desired height of the surface to create
+ */
+ @WorkerThread
+ fun onSurfaceCreated(
+ spec: EglSpec,
+ config: EGLConfig,
+ surface: Surface,
+ width: Int,
+ height: Int
+ ): EGLSurface =
+ // Always default to creating an EGL window surface
+ // Despite having access to the width and height here, do not explicitly
+ // pass in EGLConfigAttributes specifying the EGL_WIDTH and EGL_HEIGHT parameters
+ // as those are not accepted parameters for eglCreateWindowSurface but they are
+ // for other EGL Surface factory methods such as eglCreatePBufferSurface
+ // See accepted parameters here:
+ // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
+ // and here
+ // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
+ spec.eglCreateWindowSurface(config, surface, null)
+
+ /**
+ * Callback used to issue OpenGL drawing commands into the [EGLSurface]
+ * created in [onSurfaceCreated]. This [EGLSurface] is guaranteed to
+ * be current before this callback is invoked and [EglManager.swapAndFlushBuffers]
+ * will be invoked afterwards. If additional scratch [EGLSurface]s are used
+ * here it is up to the implementation of this method to ensure that the proper
+ * surfaces are made current and the appropriate swap buffers call is made
+ *
+ * This method is invoked on the backing thread
+ *
+ * @param eglManager Handle to EGL dependencies
+ */
+ @WorkerThread
+ fun onDrawFrame(eglManager: EglManager)
+ }
+
+ /**
+ * Adds the [android.view.Surface] to be managed by the GLThread.
+ * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+ * for rendering into the surface through [RenderCallback].
+ * Unlike the other [attach] methods that consume a [SurfaceView] or [TextureView],
+ * this method does not handle any lifecycle callbacks associated with the target surface.
+ * Therefore it is up to the consumer to properly setup/teardown resources associated with
+ * this surface.
+ *
+ * @param surface Target surface to be managed by the backing thread
+ * @param width Desired width of the [surface]
+ * @param height Desired height of the [surface]
+ * @param renderer Callbacks used to create a corresponding [EGLSurface] from the
+ * given surface as well as render content into the created [EGLSurface]
+ * @return [RenderTarget] used for subsequent requests to communicate
+ * with the provided Surface (ex. [requestRender] or [detach]).
+ *
+ * @throws IllegalStateException If this method was called when the GLThread has not started
+ * (i.e. start has not been called)
+ */
+ fun attach(surface: Surface, width: Int, height: Int, renderer: RenderCallback): RenderTarget {
+ val thread = mGLThread
+ if (thread != null) {
+ val token = sToken.getAndIncrement()
+ thread.attachSurface(token, surface, width, height, renderer)
+ return RenderTarget(token, this).also { mRenderTargets.add(it) }
+ } else {
+ throw IllegalStateException("GLThread not started, did you forget to call start?")
+ }
+ }
+
+ /**
+ * Adds the [android.view.Surface] provided by the given [SurfaceView] to be managed by the
+ * backing thread.
+ *
+ * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+ * for rendering into the surface through [RenderCallback].
+ *
+ * This method automatically configures a [SurfaceHolder.Callback] used to attach the
+ * [android.view.Surface] when the underlying [SurfaceHolder] that contains the surface is
+ * available. Similarly this surface will be detached from [GLRenderer] when the surface provided
+ * by the [SurfaceView] is destroyed (i.e. [SurfaceHolder.Callback.surfaceDestroyed] is called.
+ *
+ * If the [android.view.Surface] is already available by the time this method is invoked,
+ * it is attached synchronously.
+ *
+ * @param surfaceView SurfaceView that provides the surface to be rendered by the backing thread
+ * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+ * given surface as well as render content into the created [EGLSurface]
+ * @return [RenderTarget] used for subsequent requests to communicate
+ * with the provided Surface (ex. [requestRender] or [detach]).
+ *
+ * @throws IllegalStateException If this method was called when the GLThread has not started
+ * (i.e. start has not been called)
+ */
+ fun attach(surfaceView: SurfaceView, renderer: RenderCallback): RenderTarget {
+ val thread = mGLThread
+ if (thread != null) {
+ val token = sToken.getAndIncrement()
+ val holder = surfaceView.holder
+ val callback = object : SurfaceHolder.Callback2 {
+
+ var isAttached = false
+
+ /**
+ * Optional condition that maybe used if we are issuing a blocking call to render
+ * in [SurfaceHolder.Callback2.surfaceRedrawNeeded]
+ * In this case we need to signal the condition of either the request to render
+ * has completed, or if the RenderTarget has been detached and the pending
+ * render request is cancelled.
+ */
+ @Volatile var renderLatch: CountDownLatch? = null
+
+ val renderTarget = RenderTarget(token, this@GLRenderer) @WorkerThread {
+ isAttached = false
+ // SurfaceHolder.add/remove callback is thread safe
+ holder.removeCallback(this)
+ // Countdown in case we have been detached while waiting for a render
+ // to be completed
+ renderLatch?.countDown()
+ }
+
+ override fun surfaceRedrawNeeded(p0: SurfaceHolder) {
+ val latch = CountDownLatch(1).also { renderLatch = it }
+ // Request a render and block until the rendering is complete
+ // surfaceRedrawNeeded is invoked on older API levels and is replaced with
+ // surfaceRedrawNeededAsync for newer API levels which is non-blocking
+ renderTarget.requestRender @WorkerThread {
+ latch.countDown()
+ }
+ latch.await()
+ renderLatch = null
+ }
+
+ override fun surfaceRedrawNeededAsync(
+ holder: SurfaceHolder,
+ drawingFinished: Runnable
+ ) {
+ renderTarget.requestRender {
+ drawingFinished.run()
+ }
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ // NO-OP wait until surfaceChanged which is guaranteed to be called and also
+ // provides the appropriate width height of the surface
+ }
+
+ override fun surfaceChanged(
+ holder: SurfaceHolder,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ if (!isAttached) {
+ thread.attachSurface(token, holder.surface, width, height, renderer)
+ isAttached = true
+ } else {
+ renderTarget.resize(width, height)
+ }
+ renderTarget.requestRender()
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ val detachLatch = CountDownLatch(1)
+ renderTarget.detach(true) {
+ detachLatch.countDown()
+ }
+ detachLatch.await()
+ }
+ }
+ holder.addCallback(callback)
+ if (holder.surface != null && holder.surface.isValid) {
+ thread.attachSurface(
+ token,
+ holder.surface,
+ surfaceView.width,
+ surfaceView.height,
+ renderer
+ )
+ }
+ mRenderTargets.add(callback.renderTarget)
+ return callback.renderTarget
+ } else {
+ throw IllegalStateException("GLThread not started, did you forget to call start?")
+ }
+ }
+
+ /**
+ * Adds the [android.view.Surface] provided by the given [TextureView] to be managed by the
+ * backing thread.
+ *
+ * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+ * for rendering into the surface through [RenderCallback].
+ *
+ * This method automatically configures a [TextureView.SurfaceTextureListener] used to create a
+ * [android.view.Surface] when the underlying [SurfaceTexture] is available.
+ * Similarly this surface will be detached from [GLRenderer] if the underlying [SurfaceTexture]
+ * is destroyed (i.e. [TextureView.SurfaceTextureListener.onSurfaceTextureDestroyed] is called.
+ *
+ * If the [SurfaceTexture] is already available by the time this method is called, then it is
+ * attached synchronously.
+ *
+ * @param textureView TextureView that provides the surface to be rendered into on the GLThread
+ * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+ * given surface as well as render content into the created [EGLSurface]
+ * @return [RenderTarget] used for subsequent requests to communicate
+ * with the provided Surface (ex. [requestRender] or [detach]).
+ *
+ * @throws IllegalStateException If this method was called when the GLThread has not started
+ * (i.e. start has not been called)
+ */
+ fun attach(textureView: TextureView, renderer: RenderCallback): RenderTarget {
+ val thread = mGLThread
+ if (thread != null) {
+ val token = sToken.getAndIncrement()
+ val renderTarget = RenderTarget(token, this) @WorkerThread {
+ textureView.handler?.post {
+ textureView.surfaceTextureListener = null
+ }
+ }
+ textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+ override fun onSurfaceTextureAvailable(
+ surfaceTexture: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ thread.attachSurface(token, Surface(surfaceTexture), width, height, renderer)
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ texture: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ renderTarget.resize(width, height)
+ renderTarget.requestRender()
+ }
+
+ override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean {
+ val detachLatch = CountDownLatch(1)
+ renderTarget.detach(true) {
+ detachLatch.countDown()
+ }
+ detachLatch.await()
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(p0: SurfaceTexture) {
+ // NO-OP
+ }
+ }
+ if (textureView.isAvailable) {
+ thread.attachSurface(
+ token,
+ Surface(textureView.surfaceTexture),
+ textureView.width,
+ textureView.height,
+ renderer
+ )
+ }
+ mRenderTargets.add(renderTarget)
+ return renderTarget
+ } else {
+ throw IllegalStateException("GLThread not started, did you forget to call start?")
+ }
+ }
+
+ /**
+ * Handle to a [android.view.Surface] that is given to [GLRenderer] to handle
+ * rendering.
+ */
+ class RenderTarget internal constructor(
+ internal val token: Int,
+ glManager: GLRenderer,
+ @WorkerThread internal val onDetach: () -> Unit = {}
+ ) {
+
+ @Volatile
+ private var mManager: GLRenderer? = glManager
+
+ internal fun release() {
+ mManager = null
+ }
+
+ /**
+ * Request that this [RenderTarget] should have its contents redrawn.
+ * This consumes an optional callback that is invoked on the backing thread when
+ * the rendering is completed.
+ *
+ * Note the render operation will only occur if the RenderTarget is attached, that is
+ * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+ * created this RenderTarget is stopped, this is a no-op.
+ *
+ * @param onRenderComplete Optional callback called on the backing thread when
+ * rendering is finished
+ */
+ @JvmOverloads
+ fun requestRender(@WorkerThread onRenderComplete: ((RenderTarget) -> Unit)? = null) {
+ mManager?.requestRender(this@RenderTarget, onRenderComplete)
+ }
+
+ /**
+ * Determines if the current RenderTarget is attached to GLRenderer.
+ * This is true until [detach] has been called. If the RenderTarget is no longer
+ * in an attached state (i.e. this returns false). Subsequent calls to [requestRender]
+ * will be ignored.
+ */
+ fun isAttached(): Boolean = mManager != null
+
+ /**
+ * Resize the RenderTarget to the specified width and height.
+ * This will destroy the EGLSurface created by [RenderCallback.onSurfaceCreated]
+ * and invoke it again with the updated dimensions.
+ * An optional callback is invoked on the backing thread after the resize operation
+ * is complete
+ *
+ * Note the resize operation will only occur if the RenderTarget is attached, that is
+ * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+ * created this RenderTarget is stopped, this is a no-op.
+ *
+ * @param width New target width to resize the RenderTarget
+ * @param height New target height to resize the RenderTarget
+ * @param onResizeComplete Optional callback invoked after the resize is complete
+ */
+ @JvmOverloads
+ fun resize(
+ width: Int,
+ height: Int,
+ @WorkerThread onResizeComplete: ((RenderTarget) -> Unit)? = null
+ ) {
+ mManager?.resize(this, width, height, onResizeComplete)
+ }
+
+ /**
+ * Removes the corresponding [RenderTarget] from management of the GLThread.
+ * This destroys the EGLSurface associated with this surface and subsequent requests
+ * to render into the surface with the provided token are ignored.
+ *
+ * If the [cancelPending] flag is set to true, any queued request
+ * to render that has not started yet is cancelled. However, if this is invoked in the
+ * middle of the frame being rendered, it will continue to process the current frame.
+ *
+ * Additionally if this flag is false, all pending requests to render will be processed
+ * before the [RenderTarget] is detached.
+ *
+ * This is a convenience method around [GLRenderer.detach]
+ *
+ * Note the detach operation will only occur if the RenderTarget is attached, that is
+ * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+ * created this RenderTarget is stopped, this is a no-op.
+ */
+ @JvmOverloads
+ fun detach(cancelPending: Boolean, onDetachComplete: ((RenderTarget) -> Unit)? = null) {
+ mManager?.detach(this, cancelPending, onDetachComplete)
+ }
+ }
+
+ companion object {
+ /**
+ * Counter used to issue unique identifiers for surfaces that are managed by GLRenderer
+ */
+ private val sToken = AtomicInteger()
+ }
+}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
new file mode 100644
index 0000000..368ecf0
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2022 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 androidx.graphics.opengl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.util.Log
+import android.view.Surface
+import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.GLRenderer.EglContextCallback
+import androidx.graphics.opengl.GLRenderer.RenderCallback
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Thread responsible for management of EGL dependencies, setup and teardown
+ * of EGLSurface instances as well as delivering callbacks to draw a frame
+ */
+internal class GLThread(
+ name: String = "GLThread",
+ private val mEglSpecFactory: () -> EglSpec,
+ private val mEglConfigFactory: EglManager.() -> EGLConfig,
+) : HandlerThread(name) {
+
+ // Accessed on internal HandlerThread
+ private val mIsTearingDown = AtomicBoolean(false)
+ private var mEglManager: EglManager? = null
+ private val mSurfaceSessions = HashMap<Int, SurfaceSession>()
+ private var mHandler: Handler? = null
+ private val mEglContextCallback = HashSet<EglContextCallback>()
+
+ override fun start() {
+ super.start()
+ mHandler = Handler(looper)
+ }
+
+ /**
+ * Adds the given [android.view.Surface] to be managed by the GLThread.
+ * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+ * for rendering into the surface through [RenderCallback].
+ * @param surface intended to be be rendered into on the GLThread
+ * @param width Desired width of the [surface]
+ * @param height Desired height of the [surface]
+ * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+ * given surface as well as render content into the created [EGLSurface]
+ * @return Identifier used for subsequent requests to communicate
+ * with the provided Surface (ex. [requestRender] or [detachSurface]
+ */
+ @AnyThread
+ fun attachSurface(
+ token: Int,
+ surface: Surface,
+ width: Int,
+ height: Int,
+ renderer: RenderCallback
+ ) {
+ withHandler {
+ post(token) {
+ attachSurfaceSessionInternal(
+ SurfaceSession(token, surface, renderer).apply {
+ this.width = width
+ this.height = height
+ }
+ )
+ }
+ }
+ }
+
+ @AnyThread
+ fun resizeSurface(token: Int, width: Int, height: Int, callback: Runnable? = null) {
+ withHandler {
+ post(token) {
+ resizeSurfaceSessionInternal(token, width, height)
+ requestRenderInternal(token)
+ callback?.run()
+ }
+ }
+ }
+
+ @AnyThread
+ fun addEglCallbacks(callbacks: ArrayList<EglContextCallback>) {
+ withHandler {
+ post {
+ mEglContextCallback.addAll(callbacks)
+ mEglManager?.let {
+ for (callback in callbacks) {
+ callback.onEglContextCreated(it)
+ }
+ }
+ }
+ }
+ }
+
+ @AnyThread
+ fun addEglCallback(callbacks: EglContextCallback) {
+ withHandler {
+ post {
+ mEglContextCallback.add(callbacks)
+ // If EGL dependencies are already initialized, immediately invoke
+ // the added callback
+ mEglManager?.let {
+ callbacks.onEglContextCreated(it)
+ }
+ }
+ }
+ }
+
+ @AnyThread
+ fun removeEglCallback(callbacks: EglContextCallback) {
+ withHandler {
+ post {
+ mEglContextCallback.remove(callbacks)
+ }
+ }
+ }
+
+ /**
+ * Removes the corresponding [android.view.Surface] from management of the GLThread.
+ * This destroys the EGLSurface associated with this surface and subsequent requests
+ * to render into the surface with the provided token are ignored. Any queued request
+ * to render to the corresponding [SurfaceSession] that has not started yet is cancelled.
+ * However, if this is invoked in the middle of the frame being rendered, it will continue to
+ * process the current frame.
+ */
+ @AnyThread
+ fun detachSurface(
+ token: Int,
+ cancelPending: Boolean,
+ callback: Runnable?
+ ) {
+ log("dispatching request to detach surface w/ token: $token")
+ withHandler {
+ if (cancelPending) {
+ removeCallbacksAndMessages(token)
+ }
+ post(token) {
+ detachSurfaceSessionInternal(token, callback)
+ }
+ }
+ }
+
+ /**
+ * Cancel all pending requests to all currently managed [SurfaceSession] instances,
+ * destroy all EGLSurfaces, teardown EGLManager and quit this thread
+ */
+ @AnyThread
+ fun tearDown(cancelPending: Boolean, callback: Runnable?) {
+ withHandler {
+ if (cancelPending) {
+ removeCallbacksAndMessages(null)
+ }
+ post {
+ releaseResourcesInternalAndQuit(callback)
+ }
+ mIsTearingDown.set(true)
+ }
+ }
+
+ /**
+ * Mark the corresponding surface session with the given token as dirty
+ * to schedule a call to [RenderCallback#onDrawFrame].
+ * If there is already a queued request to render into the provided surface with
+ * the specified token, this request is ignored.
+ */
+ @AnyThread
+ fun requestRender(token: Int, callback: Runnable? = null) {
+ log("dispatching request to render for token: $token")
+ withHandler {
+ post(token) {
+ requestRenderInternal(token)
+ callback?.run()
+ }
+ }
+ }
+
+ /**
+ * Lazily creates an [EglManager] instance from the given [mEglSpecFactory]
+ * used to determine the configuration. This result is cached across calls
+ * unless [tearDown] has been called.
+ */
+ @WorkerThread
+ fun obtainEglManager(): EglManager =
+ mEglManager ?: EglManager(mEglSpecFactory.invoke()).also {
+ it.initialize()
+ val config = mEglConfigFactory.invoke(it)
+ it.createContext(config)
+ for (callback in mEglContextCallback) {
+ callback.onEglContextCreated(it)
+ }
+ mEglManager = it
+ }
+
+ @WorkerThread
+ private fun disposeSurfaceSession(session: SurfaceSession) {
+ val eglSurface = session.eglSurface
+ if (eglSurface != null && eglSurface != EGL14.EGL_NO_SURFACE) {
+ obtainEglManager().eglSpec.eglDestroySurface(eglSurface)
+ session.eglSurface = null
+ }
+ }
+
+ /**
+ * Helper method to obtain the cached EGLSurface for the given [SurfaceSession],
+ * creating one if it does not previously exist
+ */
+ @WorkerThread
+ private fun obtainEglSurfaceForSession(session: SurfaceSession): EGLSurface =
+ session.eglSurface ?: createEglSurfaceForSession(session)
+
+ /**
+ * Helper method to create the corresponding EGLSurface from the [SurfaceSession] instance
+ * Consumers are expected to teardown the previously existing EGLSurface instance if it exists
+ */
+ @WorkerThread
+ private fun createEglSurfaceForSession(
+ session: SurfaceSession
+ ): EGLSurface {
+ with(obtainEglManager()) {
+ val eglSurface = session.surfaceRenderer.onSurfaceCreated(
+ eglSpec,
+ // Successful creation of EglManager ensures non null EGLConfig
+ eglConfig!!,
+ session.surface,
+ session.width,
+ session.height
+ )
+ session.eglSurface = eglSurface
+ return eglSurface
+ }
+ }
+
+ @WorkerThread
+ private fun releaseResourcesInternalAndQuit(callback: Runnable?) {
+ mEglManager?.let { eglManager ->
+ for (session in mSurfaceSessions) {
+ disposeSurfaceSession(session.value)
+ }
+ callback?.run()
+ mSurfaceSessions.clear()
+ for (eglCallback in mEglContextCallback) {
+ eglCallback.onEglContextDestroyed(eglManager)
+ }
+ mEglContextCallback.clear()
+ eglManager.release()
+ mEglManager = null
+ quit()
+ }
+ }
+
+ @WorkerThread
+ private fun requestRenderInternal(token: Int) {
+ log("requesting render for token: $token")
+ mSurfaceSessions[token]?.let { surfaceSession ->
+ val eglManager = obtainEglManager()
+ eglManager.makeCurrent(obtainEglSurfaceForSession(surfaceSession))
+ val width = surfaceSession.width
+ val height = surfaceSession.height
+ if (width > 0 && height > 0) {
+ surfaceSession.surfaceRenderer.onDrawFrame(eglManager)
+ }
+ eglManager.swapAndFlushBuffers()
+ }
+ }
+
+ @WorkerThread
+ private fun attachSurfaceSessionInternal(surfaceSession: SurfaceSession) {
+ mSurfaceSessions[surfaceSession.surfaceToken] = surfaceSession
+ }
+
+ @WorkerThread
+ private fun resizeSurfaceSessionInternal(
+ token: Int,
+ width: Int,
+ height: Int
+ ) {
+ mSurfaceSessions[token]?.let { surfaceSession ->
+ surfaceSession.apply {
+ this.width = width
+ this.height = height
+ }
+ disposeSurfaceSession(surfaceSession)
+ obtainEglSurfaceForSession(surfaceSession)
+ }
+ }
+
+ @WorkerThread
+ private fun detachSurfaceSessionInternal(token: Int, callback: Runnable?) {
+ val session = mSurfaceSessions.remove(token)
+ if (session != null) {
+ disposeSurfaceSession(session)
+ }
+ callback?.run()
+ }
+
+ /**
+ * Helper method that issues a callback on the handler instance for this thread
+ * ensuring proper nullability checks are handled.
+ * This assumes that that [GLRenderer.start] has been called before attempts
+ * to interact with the corresponding Handler are made with this method
+ */
+ private inline fun withHandler(block: Handler.() -> Unit) {
+ val handler = mHandler
+ ?: throw IllegalStateException("Did you forget to call GLThread.start()?")
+ if (!mIsTearingDown.get()) {
+ block(handler)
+ }
+ }
+
+ companion object {
+
+ private const val DEBUG = true
+ private const val TAG = "GLThread"
+ internal fun log(msg: String) {
+ if (DEBUG) {
+ Log.v(TAG, msg)
+ }
+ }
+ }
+
+ private class SurfaceSession(
+ /**
+ * Identifier used to lookup the mapping of this surface session.
+ * Consumers are expected to provide this identifier to operate on the corresponding
+ * surface to either request a frame be rendered or to remove this Surface
+ */
+ val surfaceToken: Int,
+
+ /**
+ * Target surface to render into
+ */
+ val surface: Surface,
+
+ /**
+ * Callback used to create an EGLSurface from the provided surface as well as
+ * render content to the surface
+ */
+ val surfaceRenderer: RenderCallback
+ ) {
+ /**
+ * Lazily created + cached [EGLSurface] after [RenderCallback.onSurfaceCreated]
+ * is invoked. This is only modified on the backing thread
+ */
+ var eglSurface: EGLSurface? = null
+
+ /**
+ * Target width of the [surface]. This is only modified on the backing thread
+ */
+ var width: Int = 0
+
+ /**
+ * Target height of the [surface]. This is only modified on the backing thread
+ */
+ var height: Int = 0
+ }
+
+ /**
+ * Handler does not expose a post method that takes a token and a runnable.
+ * We need the token to be able to cancel pending requests so just call
+ * postAtTime with the default of SystemClock.uptimeMillis
+ */
+ private fun Handler.post(token: Any?, runnable: Runnable) {
+ postAtTime(runnable, token, SystemClock.uptimeMillis())
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt
new file mode 100644
index 0000000..883b2e8
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.opengl.EGL14
+
+/**
+ * EGL configuration attribute used to expose EGLConfigs that support formats with floating point
+ * RGBA components. This attribute is exposed through the EGL_EXT_pixel_format_float EGL extension
+ *
+ * See: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_pixel_format_float.txt
+ */
+const val EglColorComponentTypeExt = 0x3339
+
+/**
+ * EGL configuration attribute value that represents fixed point RGBA components
+ */
+const val EglColorComponentTypeFixedExt = 0x333A
+
+/**
+ * EGL configuration attribute value that represents floating point RGBA components
+ */
+const val EglColorComponentTypeFloatExt = 0x333B
+
+/**
+ * EGL Attributes to create an 8 bit EGL config for red, green, blue, and alpha channels as well
+ * as an 8 bit stencil size
+ */
+val EglConfigAttributes8888 = EglConfigAttributes {
+ EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+ EGL14.EGL_RED_SIZE to 8
+ EGL14.EGL_GREEN_SIZE to 8
+ EGL14.EGL_BLUE_SIZE to 8
+ EGL14.EGL_ALPHA_SIZE to 8
+ EGL14.EGL_DEPTH_SIZE to 0
+ EGL14.EGL_CONFIG_CAVEAT to EGL14.EGL_NONE
+ EGL14.EGL_STENCIL_SIZE to 0
+ EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * EGL Attributes to create a 10 bit EGL config for red, green, blue, channels and a
+ * 2 bit alpha channels as well as an 8 bit stencil size
+ */
+val EglConfigAttributes1010102 = EglConfigAttributes {
+ EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+ EGL14.EGL_RED_SIZE to 10
+ EGL14.EGL_GREEN_SIZE to 10
+ EGL14.EGL_BLUE_SIZE to 10
+ EGL14.EGL_ALPHA_SIZE to 2
+ EGL14.EGL_DEPTH_SIZE to 0
+ EGL14.EGL_STENCIL_SIZE to 0
+ EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * EGL Attributes to create a 16 bit floating point EGL config for red, green and blue channels
+ * along with a
+ */
+val EglConfigAttributesF16 = EglConfigAttributes {
+ EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+ EglColorComponentTypeExt to EglColorComponentTypeFloatExt
+ EGL14.EGL_RED_SIZE to 16
+ EGL14.EGL_GREEN_SIZE to 16
+ EGL14.EGL_BLUE_SIZE to 16
+ EGL14.EGL_ALPHA_SIZE to 16
+ EGL14.EGL_DEPTH_SIZE to 0
+ EGL14.EGL_STENCIL_SIZE to 0
+ EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * Construct an instance of [EglConfigAttributes] that includes a mapping of EGL attributes
+ * to their corresponding value. The full set of attributes can be found here:
+ * https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglChooseConfig.xhtml
+ *
+ * The resultant array of attributes automatically is terminated with EGL_NONE.
+ *
+ * For example to create an 8888 configuration, this can be done with the following:
+ *
+ * EglConfigAttributes {
+ * EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+ * EGL14.EGL_RED_SIZE to 8
+ * EGL14.EGL_GREEN_SIZE to 8
+ * EGL14.EGL_BLUE_SIZE to 8
+ * EGL14.EGL_ALPHA_SIZE to 8
+ * EGL14.EGL_DEPTH_SIZE to 0
+ * EGL14.EGL_CONFIG_CAVEAT to EGL14.EGL_NONE
+ * EGL14.EGL_STENCIL_SIZE to 8
+ * EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+ * }
+ *
+ * @see EglConfigAttributes8888
+ */
+inline fun EglConfigAttributes(block: EglConfigAttributes.Builder.() -> Unit): EglConfigAttributes =
+ EglConfigAttributes.Builder().apply { block() }.build()
+
+// klint does not support value classes yet, see b/197692691
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class EglConfigAttributes internal constructor(
+ @PublishedApi internal val attrs: IntArray
+) {
+
+ /**
+ * Builder used to create an instance of [EglConfigAttributes]
+ * Allows for a mapping of EGL configuration attributes to their corresponding
+ * values as well as including a previously generated [EglConfigAttributes]
+ * instance to be used as a template and conditionally update individual mapped values
+ */
+ // Suppressing build method as EglConfigAttributes is created using Kotlin DSL syntax
+ // via the function constructor defined above
+ @SuppressWarnings("MissingBuildMethod")
+ class Builder @PublishedApi internal constructor() {
+ private val attrs = HashMap<Int, Int>()
+
+ /**
+ * Map a given EGL configuration attribute key to the given EGL configuration value
+ */
+ @SuppressWarnings("BuilderSetStyle")
+ infix fun Int.to(that: Int) {
+ attrs[this] = that
+ }
+
+ /**
+ * Include all the attributes of the given EglConfigAttributes instance.
+ * This is useful for creating a new EglConfigAttributes instance with all the same
+ * attributes as another, allowing for modification of attributes after the fact.
+ * For example, the following code snippet can be used to create an EglConfigAttributes
+ * instance that has all the same configuration as [EglConfigAttributes8888] but with a
+ * 16 bit stencil buffer size:
+ *
+ * EglConfigAttributes {
+ * include(EglConfigAttributes8888)
+ * EGL14.EGL_STENCIL_SIZE to 16
+ * }
+ *
+ *
+ * That is all attributes configured after the include will overwrite the attributes
+ * configured previously.
+ */
+ @SuppressWarnings("BuilderSetStyle")
+ fun include(attributes: EglConfigAttributes) {
+ val attrsArray = attributes.attrs
+ for (i in 0 until attrsArray.size - 1 step 2) {
+ attrs[attrsArray[i]] = attrsArray[i + 1]
+ }
+ }
+
+ @PublishedApi internal fun build(): EglConfigAttributes {
+ val entries = attrs.entries
+ val attrArray = IntArray(entries.size * 2 + 1) // Array must end with EGL_NONE
+ var index = 0
+ for (entry in entries) {
+ attrArray[index] = entry.key
+ attrArray[index + 1] = entry.value
+ index += 2
+ }
+ attrArray[index] = EGL14.EGL_NONE
+ return EglConfigAttributes(attrArray)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt
new file mode 100644
index 0000000..1c7cdca
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+/**
+ * Determines if applications can query the age of the back buffer contents for an
+ * EGL surface as the number of frames elapsed since the contents were recently defined
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_buffer_age.txt
+ */
+const val EglExtBufferAge = "EGL_EXT_buffer_age"
+
+/**
+ * Allows for efficient partial updates to an area of a **buffer** that has changed since
+ * the last time the buffer was used
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_partial_update.txt
+ */
+const val EglKhrPartialUpdate = "EGL_KHR_partial_update"
+
+/**
+ * Allows for efficient partial updates to an area of a **surface** that changes between
+ * frames for the surface. This relates to the differences between two buffers, the current
+ * back buffer and the current front buffer.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_swap_buffers_with_damage.txt
+ */
+const val EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage"
+
+/**
+ * Determines whether to use sRGB format default framebuffers to render sRGB
+ * content to display devices. Supports creation of EGLSurfaces which will be rendered to in
+ * sRGB by OpenGL contexts supporting that capability.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
+ */
+const val EglKhrGlColorSpace = "EGL_KHR_gl_colorspace"
+
+/**
+ * Determines whether creation of GL and ES contexts without an EGLConfig is allowed
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_no_config_context.txt
+ */
+const val EglKhrNoConfigContext = "EGL_KHR_no_config_context"
+
+/**
+ * Determines whether floating point RGBA components are supported
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_pixel_format_float.txt
+ */
+const val EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float"
+
+/**
+ * Determines whether extended sRGB color spaces are supported options for EGL Surfaces
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_scrgb.txt
+ */
+const val EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb"
+
+/**
+ * Determines whether the underlying platform can support rendering framebuffers in the
+ * non-linear Display-P3 color space
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_display_p3_passthrough.txt
+ */
+const val EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough"
+
+/**
+ * Determines whether the platform framebuffers support rendering in a larger color gamut
+ * specified in the BT.2020 color space
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
+ */
+const val EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq"
+
+/**
+ * Determines whether an EGLContext can be created with a priority hint. Not all implementations
+ * are guaranteed to honor the hint.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/IMG/EGL_IMG_context_priority.txt
+ */
+const val EglImgContextPriority = "EGL_IMG_context_priority"
+
+/**
+ * Determines whether creation of an EGL Context without a surface is supported.
+ * This is useful for applications that only want to render to client API targets (such as
+ * OpenGL framebuffer objects) and avoid the need to a throw-away EGL surface just to get
+ * a current context.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
+ */
+const val EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context"
+
+/**
+ * Determines whether sync objects are supported. Sync objects are synchronization primitives
+ * that represent events whose completion can be tested or waited upon.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_fence_sync.txt
+ */
+const val EglKhrFenceSync = "EGL_KHR_fence_sync"
+
+/**
+ * Determines whether waiting for signaling of sync objects is supported. This form of wait
+ * does not necessarily block the application thread which issued the wait. Therefore
+ * applications may continue to issue commands to the client API or perform other work
+ * in parallel leading to increased performance.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_wait_sync.txt
+ */
+const val EglKhrWaitSync = "EGL_KHR_wait_sync"
+
+/**
+ * Determines whether creation of platform specific sync objects are supported. These
+ * objects that are associated with a native synchronization fence object using a file
+ * descriptor.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
+ */
+const val EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync"
+
+/**
+ * Class determining the types of OpenGL extensions supported by the given
+ * EGL spec.
+ */
+// klint does not support value classes yet, see b/197692691
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class EglExtensions(
+ private val extensionSet: Set<String>
+) {
+
+ /**
+ * Determines whether the extension with the provided name is supported. The string
+ * provided is expected to be one of the named extensions defined within the OpenGL
+ * extension documentation.
+ *
+ * Returns true if the extension is supported, false otherwise
+ */
+ fun isExtensionSupported(extensionName: String): Boolean =
+ extensionSet.contains(extensionName)
+
+ companion object {
+
+ /**
+ * Creates an instance of [EglExtensions] from a space separated string
+ * that represents the set of OpenGL extensions supported
+ */
+ @JvmStatic
+ fun from(queryString: String): EglExtensions =
+ HashSet<String>().let {
+ it.addAll(queryString.split(' '))
+ EglExtensions(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt
new file mode 100644
index 0000000..da3e7c5
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+
+/**
+ * Class responsible for configuration of EGL related resources. This includes
+ * initialization of the corresponding EGL Display as well as EGL Context, among
+ * other EGL related facilities.
+ */
+class EglManager(val eglSpec: EglSpec = EglSpec.Egl14) {
+
+ private var mEglConfig: EGLConfig? = null
+
+ /**
+ * Offscreen pixel buffer surface
+ */
+ private var mPBufferSurface: EGLSurface = EGL14.EGL_NO_SURFACE
+ private var mEglContext: EGLContext = EGL14.EGL_NO_CONTEXT
+ private var mWideColorGamutSupport = false
+ private var mEglVersion = EglVersion.Unknown
+ private var mEglExtensions: EglExtensions? = null
+ private var mIsSingleBuffered: Boolean = false
+ private var mQueryResult: IntArray? = null
+
+ /**
+ * Initialize the EGLManager. This initializes the default display as well
+ * as queries the supported extensions
+ */
+ fun initialize() {
+ mEglContext.let {
+ if (it === EGL14.EGL_NO_CONTEXT) {
+ mEglVersion = eglSpec.eglInitialize()
+ mEglExtensions = EglExtensions.from(eglSpec.eglQueryString(EGL14.EGL_EXTENSIONS))
+ }
+ }
+ }
+
+ /**
+ * Attempt to load an [EGLConfig] instance from the given
+ * [EglConfigAttributes]. If the [EGLConfig] could not be loaded
+ * this returns null
+ */
+ fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig? =
+ eglSpec.loadConfig(configAttributes)
+
+ /**
+ * Creates an [EGLContext] from the given [EGLConfig] returning
+ * null if the context could not be created
+ *
+ * @throws EglException if the default surface could not be made current after context creation
+ */
+ fun createContext(config: EGLConfig): EGLContext {
+ val eglContext = eglSpec.eglCreateContext(config)
+ if (eglContext !== EGL14.EGL_NO_CONTEXT) {
+ val pbBufferSurface: EGLSurface = if (isExtensionSupported(EglKhrSurfacelessContext)) {
+ EGL14.EGL_NO_SURFACE
+ } else {
+ val configAttrs = EglConfigAttributes {
+ EGL14.EGL_WIDTH to 1
+ EGL14.EGL_HEIGHT to 1
+ }
+ eglSpec.eglCreatePBufferSurface(config, configAttrs)
+ }
+ if (!eglSpec.eglMakeCurrent(eglContext, pbBufferSurface, pbBufferSurface)) {
+ throw EglException(eglSpec.eglGetError(), "Unable to make default surface current")
+ }
+ mPBufferSurface = pbBufferSurface
+ mEglContext = eglContext
+ mEglConfig = config
+ } else {
+ mPBufferSurface = EGL14.EGL_NO_SURFACE
+ mEglContext = EGL14.EGL_NO_CONTEXT
+ mEglConfig = null
+ }
+ return eglContext
+ }
+
+ /**
+ * Release the resources allocated by EGLManager. This will destroy the corresponding
+ * EGLContext instance if it was previously initialized.
+ * The configured EGLVersion as well as EGLExtensions
+ */
+ fun release() {
+ mEglContext.let {
+ if (it != EGL14.EGL_NO_CONTEXT) {
+ eglSpec.eglDestroyContext(it)
+ mPBufferSurface.let { pbBufferSurface ->
+ if (pbBufferSurface != EGL14.EGL_NO_SURFACE) {
+ eglSpec.eglDestroySurface(pbBufferSurface)
+ }
+ }
+ mPBufferSurface = EGL14.EGL_NO_SURFACE
+ eglSpec.eglMakeCurrent(
+ EGL14.EGL_NO_CONTEXT,
+ EGL14.EGL_NO_SURFACE,
+ EGL14.EGL_NO_SURFACE
+ )
+ mEglVersion = EglVersion.Unknown
+ mEglContext = EGL14.EGL_NO_CONTEXT
+ mEglConfig = null
+ mEglExtensions = null
+ }
+ }
+ }
+
+ /**
+ * Returns the EGL version that is supported. This parameter is configured
+ * after [initialize] is invoked.
+ */
+ val eglVersion: EglVersion
+ get() = mEglVersion
+
+ /**
+ * Returns the current EGLContext. This parameter is configured after [initialize] is invoked
+ */
+ val eglContext: EGLContext?
+ get() = mEglContext
+
+ /**
+ * Returns the [EGLConfig] used to load the current [EGLContext].
+ * This is configured after [createContext] is invoked.
+ */
+ val eglConfig: EGLConfig?
+ get() = mEglConfig
+
+ /**
+ * Determines whether the extension with the provided name is supported. The string
+ * provided is expected to be one of the named extensions defined within the OpenGL
+ * extension documentation.
+ *
+ * See [EglExtensions] for additional documentation for given extension name constants
+ * and descriptions.
+ *
+ * The set of supported extensions is configured after [initialize] is invoked.
+ * Attempts to query support for any extension beforehand will return false.
+ */
+ fun isExtensionSupported(extensionName: String): Boolean =
+ mEglExtensions?.isExtensionSupported(extensionName) ?: false
+
+ /**
+ * Binds the current context to the given draw and read surfaces.
+ * The draw surface is used for all operations except for any pixel data read back or
+ * copy operations which are taken from the read surface.
+ *
+ * The same EGLSurface may be specified for both draw and read surfaces.
+ *
+ * If the context is not previously configured, the only valid parameters for the
+ * draw and read surfaces is [EGL14.EGL_NO_SURFACE]. This is useful to make sure there is
+ * always a surface specified and to release the current context without assigning a new one.
+ *
+ * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml
+ *
+ * @param drawSurface Surface used for all operations that involve writing pixel information
+ * @param readSurface Surface used for pixel data read back or copy operations. By default this
+ * is the same as [drawSurface]
+ */
+ @JvmOverloads
+ fun makeCurrent(drawSurface: EGLSurface, readSurface: EGLSurface = drawSurface): Boolean {
+ val result = eglSpec.eglMakeCurrent(mEglContext, drawSurface, readSurface)
+ if (result) {
+ querySurface(drawSurface)
+ }
+ return result
+ }
+
+ /**
+ * Post EGL surface color buffer to a native window. If the current drawing surface
+ * is single buffered this will flush the buffer
+ */
+ fun swapAndFlushBuffers() {
+ if (mIsSingleBuffered) {
+ GLES20.glFlush()
+ }
+ eglSpec.eglSwapBuffers(currentDrawSurface)
+ }
+
+ /**
+ * Returns the default surface. This can be an offscreen pixel buffer surface or
+ * [EGL14.EGL_NO_SURFACE] if the surfaceless context extension is supported.
+ */
+ val defaultSurface: EGLSurface
+ get() = mPBufferSurface
+
+ /**
+ * Returns the current surface used for drawing pixel content
+ */
+ val currentDrawSurface: EGLSurface
+ get() = eglSpec.eglGetCurrentDrawSurface()
+
+ /**
+ * Returns the current surface used for reading back or copying pixels
+ */
+ val currentReadSurface: EGLSurface
+ get() = eglSpec.eglGetCurrentReadSurface()
+
+ /**
+ * Helper method to query properties of the given surface
+ */
+ private fun querySurface(surface: EGLSurface) {
+ val resultArray = mQueryResult ?: IntArray(1).also { mQueryResult = it }
+ if (eglSpec.eglQuerySurface(surface, EGL14.EGL_RENDER_BUFFER, resultArray, 0)) {
+ mIsSingleBuffered = resultArray[0] == EGL14.EGL_SINGLE_BUFFER
+ }
+ }
+
+ companion object {
+ private const val TAG = "EglManager"
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt
new file mode 100644
index 0000000..b1e9881
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt
@@ -0,0 +1,399 @@
+/*
+ * Copyright 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 androidx.graphics.opengl.egl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLSurface
+import android.view.Surface
+
+/**
+ * Interface for accessing various EGL facilities independent of EGL versions.
+ * That is each EGL version implements this specification.
+ *
+ * EglSpec is not thread safe and is up to the caller of these methods to guarantee thread safety.
+ */
+interface EglSpec {
+
+ /**
+ * Query for the capabilities associated with the given eglDisplay.
+ * The result contains a space separated list of the capabilities.
+ *
+ * @param nameId identifier for the EGL string to query
+ */
+ fun eglQueryString(nameId: Int): String
+
+ /**
+ * Create a Pixel Buffer surface with the corresponding [EglConfigAttributes].
+ * Accepted attributes are defined as part of the OpenGL specification here:
+ * https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
+ *
+ * If a pixel buffer surface could not be created, [EGL14.EGL_NO_SURFACE] is returned.
+ *
+ * @param config Specifies the EGL Frame buffer configuration that defines the frame buffer
+ * resource available to the surface
+ * @param configAttributes Optional list of attributes for the pixel buffer surface
+ */
+ fun eglCreatePBufferSurface(
+ config: EGLConfig,
+ configAttributes: EglConfigAttributes?
+ ): EGLSurface
+
+ /**
+ * Creates an on screen EGL window surface from the given [Surface] and returns a handle to it.
+ *
+ * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
+ *
+ * @param config Specifies the EGL frame buffer configuration that defines the frame buffer
+ * resource available to the surface
+ * @param surface Android surface to consume rendered content
+ * @param configAttributes Optional list of attributes for the specified surface
+ */
+ fun eglCreateWindowSurface(
+ config: EGLConfig,
+ surface: Surface,
+ configAttributes: EglConfigAttributes?
+ ): EGLSurface
+
+ /**
+ * Destroys an EGL surface.
+ *
+ * If the EGL surface is not current to any thread, eglDestroySurface destroys
+ * it immediately. Otherwise, surface is destroyed when it becomes not current to any thread.
+ * Furthermore, resources associated with a pbuffer surface are not released until all color
+ * buffers of that pbuffer bound to a texture object have been released. Deferral of
+ * surface destruction would still return true as deferral does not indicate a failure condition
+ *
+ * @return True if destruction of the EGLSurface was successful, false otherwise
+ */
+ fun eglDestroySurface(surface: EGLSurface): Boolean
+
+ /**
+ * Binds the current context to the given draw and read surfaces.
+ * The draw surface is used for all operations except for any pixel data read back or copy
+ * operations which are taken from the read surface.
+ *
+ * The same EGLSurface may be specified for both draw and read surfaces.
+ *
+ * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml for more
+ * information
+ *
+ * @param drawSurface EGLSurface to draw pixels into.
+ * @param readSurface EGLSurface used for read/copy operations.
+ */
+ fun eglMakeCurrent(
+ context: EGLContext,
+ drawSurface: EGLSurface,
+ readSurface: EGLSurface
+ ): Boolean
+
+ /**
+ * Return the current surface used for reading or copying pixels.
+ * If no context is current, [EGL14.EGL_NO_SURFACE] is returned
+ */
+ fun eglGetCurrentReadSurface(): EGLSurface
+
+ /**
+ * Return the current surface used for drawing pixels.
+ * If no context is current, [EGL14.EGL_NO_SURFACE] is returned.
+ */
+ fun eglGetCurrentDrawSurface(): EGLSurface
+
+ /**
+ * Initialize the EGL implementation and return the major and minor version of the EGL
+ * implementation through [EglVersion]. If initialization fails, this returns
+ * [EglVersion.Unknown]
+ */
+ fun eglInitialize(): EglVersion
+
+ /**
+ * Load a corresponding EGLConfig from the provided [EglConfigAttributes]
+ * If the EGLConfig could not be loaded, null is returned
+ * @param configAttributes Desired [EglConfigAttributes] to create an [EGLConfig]
+ *
+ * @return the [EGLConfig] with the provided [EglConfigAttributes] or null if
+ * an [EGLConfig] could not be created with the specified attributes
+ */
+ fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig?
+
+ /**
+ * Create an EGLContext with the default display. If createContext fails to create a
+ * rendering context, EGL_NO_CONTEXT is returned
+ *
+ * @param config [EGLConfig] used to create the [EGLContext]
+ */
+ fun eglCreateContext(config: EGLConfig): EGLContext
+
+ /**
+ * Destroy the given EGLContext generated in [eglCreateContext]
+ *
+ * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglDestroyContext.xhtml
+ *
+ * @param eglContext EGL rendering context to be destroyed
+ */
+ fun eglDestroyContext(eglContext: EGLContext)
+
+ /**
+ * Post EGL surface color buffer to a native window
+ *
+ * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglSwapBuffers.xhtml
+ *
+ * @param surface Specifies the EGL drawing surface whose buffers are to be swapped
+ *
+ * @return True if swapping of buffers succeeds, false otherwise
+ */
+ fun eglSwapBuffers(surface: EGLSurface): Boolean
+
+ /**
+ * Query the EGL attributes of the provided surface
+ *
+ * @param surface EGLSurface to be queried
+ * @param attribute EGL attribute to query on the given EGL Surface
+ * @param result Int array to store the result of the query
+ * @param offset Index within [result] to store the value of the queried attribute
+ *
+ * @return True if the query was completed successfully, false otherwise. If the query
+ * fails, [result] is unmodified
+ */
+ fun eglQuerySurface(surface: EGLSurface, attribute: Int, result: IntArray, offset: Int): Boolean
+
+ /**
+ * Returns the error of the last called EGL function in the current thread. Initially,
+ * the error is set to EGL_SUCCESS. When an EGL function could potentially generate several
+ * different errors (for example, when passed both a bad attribute name, and a bad attribute
+ * value for a legal attribute name), the implementation may choose to generate any one of the
+ * applicable errors.
+ *
+ * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglGetError.xhtml for more information
+ * and error codes that could potentially be returned
+ */
+ fun eglGetError(): Int
+
+ /**
+ * Convenience method to obtain the corresponding error string from the
+ * error code obtained from [EglSpec.eglGetError]
+ */
+ fun getErrorMessage(): String = getStatusString(eglGetError())
+
+ companion object {
+
+ @JvmField
+ val Egl14 = object : EglSpec {
+
+ // Tuples of attribute identifiers along with their corresponding values.
+ // EGL_NONE is used as a termination value similar to a null terminated string
+ private val contextAttributes = intArrayOf(
+ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, // GLES VERSION 2
+ // HWUI provides the ability to configure a context priority as well but that only
+ // seems to be configured on SystemUIApplication. This might be useful for
+ // front buffer rendering situations for performance.
+ EGL14.EGL_NONE
+ )
+
+ override fun eglInitialize(): EglVersion {
+ // eglInitialize is destructive so create 2 separate arrays to store the major and
+ // minor version
+ val major = intArrayOf(1)
+ val minor = intArrayOf(1)
+ val initializeResult =
+ EGL14.eglInitialize(getDefaultDisplay(), major, 0, minor, 0)
+ if (initializeResult) {
+ return EglVersion(major[0], minor[0])
+ } else {
+ throw EglException(EGL14.eglGetError(), "Unable to initialize default display")
+ }
+ }
+
+ override fun eglGetCurrentReadSurface(): EGLSurface =
+ EGL14.eglGetCurrentSurface(EGL14.EGL_READ)
+
+ override fun eglGetCurrentDrawSurface(): EGLSurface =
+ EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)
+
+ override fun eglQueryString(nameId: Int): String =
+ EGL14.eglQueryString(getDefaultDisplay(), nameId)
+
+ override fun eglCreatePBufferSurface(
+ config: EGLConfig,
+ configAttributes: EglConfigAttributes?
+ ): EGLSurface =
+ EGL14.eglCreatePbufferSurface(
+ getDefaultDisplay(),
+ config,
+ configAttributes?.attrs,
+ 0
+ )
+
+ override fun eglCreateWindowSurface(
+ config: EGLConfig,
+ surface: Surface,
+ configAttributes: EglConfigAttributes?,
+ ): EGLSurface =
+ EGL14.eglCreateWindowSurface(
+ getDefaultDisplay(),
+ config,
+ surface,
+ configAttributes?.attrs ?: DefaultWindowSurfaceConfig.attrs,
+ 0
+ )
+
+ override fun eglSwapBuffers(surface: EGLSurface): Boolean =
+ EGL14.eglSwapBuffers(getDefaultDisplay(), surface)
+
+ override fun eglQuerySurface(
+ surface: EGLSurface,
+ attribute: Int,
+ result: IntArray,
+ offset: Int
+ ): Boolean =
+ EGL14.eglQuerySurface(getDefaultDisplay(), surface, attribute, result, offset)
+
+ override fun eglDestroySurface(surface: EGLSurface) =
+ EGL14.eglDestroySurface(getDefaultDisplay(), surface)
+
+ override fun eglMakeCurrent(
+ context: EGLContext,
+ drawSurface: EGLSurface,
+ readSurface: EGLSurface
+ ): Boolean =
+ EGL14.eglMakeCurrent(
+ getDefaultDisplay(),
+ drawSurface,
+ readSurface,
+ context
+ )
+
+ override fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig? {
+ val configs = arrayOfNulls<EGLConfig?>(1)
+ return if (EGL14.eglChooseConfig(
+ getDefaultDisplay(),
+ configAttributes.attrs,
+ 0,
+ configs,
+ 0,
+ 1,
+ intArrayOf(1),
+ 0
+ )) {
+ configs[0]
+ } else {
+ null
+ }
+ }
+
+ override fun eglCreateContext(config: EGLConfig): EGLContext {
+ return EGL14.eglCreateContext(
+ getDefaultDisplay(),
+ config,
+ EGL14.EGL_NO_CONTEXT, // not creating from a shared context
+ contextAttributes,
+ 0
+ )
+ }
+
+ override fun eglDestroyContext(eglContext: EGLContext) {
+ if (!EGL14.eglDestroyContext(getDefaultDisplay(), eglContext)) {
+ throw EglException(EGL14.eglGetError(), "Unable to destroy EGLContext")
+ }
+ }
+
+ override fun eglGetError(): Int = EGL14.eglGetError()
+
+ private fun getDefaultDisplay() = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+
+ /**
+ * EglConfigAttribute that provides the default attributes for an EGL window surface
+ */
+ private val DefaultWindowSurfaceConfig = EglConfigAttributes {}
+ }
+
+ /**
+ * Return a string representation of the corresponding EGL status code.
+ * If the provided error value is not an EGL status code, the hex representation
+ * is returned instead
+ */
+ @JvmStatic
+ fun getStatusString(error: Int): String =
+ when (error) {
+ EGL14.EGL_SUCCESS -> "EGL_SUCCESS"
+ EGL14.EGL_NOT_INITIALIZED -> "EGL_NOT_INITIALIZED"
+ EGL14.EGL_BAD_ACCESS -> "EGL_BAD_ACCESS"
+ EGL14.EGL_BAD_ALLOC -> "EGL_BAD_ALLOC"
+ EGL14.EGL_BAD_ATTRIBUTE -> "EGL_BAD_ATTRIBUTE"
+ EGL14.EGL_BAD_CONFIG -> "EGL_BAD_CONFIG"
+ EGL14.EGL_BAD_CONTEXT -> "EGL_BAD_CONTEXT"
+ EGL14.EGL_BAD_CURRENT_SURFACE -> "EGL_BAD_CURRENT_SURFACE"
+ EGL14.EGL_BAD_DISPLAY -> "EGL_BAD_DISPLAY"
+ EGL14.EGL_BAD_MATCH -> "EGL_BAD_MATCH"
+ EGL14.EGL_BAD_NATIVE_PIXMAP -> "EGL_BAD_NATIVE_PIXMAP"
+ EGL14.EGL_BAD_NATIVE_WINDOW -> "EGL_BAD_NATIVE_WINDOW"
+ EGL14.EGL_BAD_PARAMETER -> "EGL_BAD_PARAMETER"
+ EGL14.EGL_BAD_SURFACE -> "EGL_BAD_SURFACE"
+ EGL14.EGL_CONTEXT_LOST -> "EGL_CONTEXT_LOST"
+ else -> Integer.toHexString(error)
+ }
+ }
+}
+
+/**
+ * Exception class for reporting errors with EGL
+ *
+ * @param error Error code reported via eglGetError
+ * @param msg Optional message describing the exception being thrown
+ */
+class EglException(val error: Int, val msg: String = "") : RuntimeException() {
+
+ override val message: String
+ get() = "Error: ${EglSpec.getStatusString(error)}, $msg"
+}
+
+/**
+ * Identifier for the current EGL implementation
+ *
+ * @param major Major version of the EGL implementation
+ * @param minor Minor version of the EGL implementation
+ */
+data class EglVersion(
+ val major: Int,
+ val minor: Int
+) {
+
+ override fun toString(): String {
+ return "EGL version $major.$minor"
+ }
+
+ companion object {
+ /**
+ * Constant that represents version 1.4 of the EGL spec
+ */
+ @JvmField
+ val V14 = EglVersion(1, 4)
+
+ /**
+ * Constant that represents version 1.5 of the EGL spec
+ */
+ @JvmField
+ val V15 = EglVersion(1, 5)
+
+ /**
+ * Sentinel EglVersion value returned in error situations
+ */
+ @JvmField
+ val Unknown = EglVersion(-1, -1)
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
new file mode 100644
index 0000000..6b86ed4
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 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 androidx.graphics.surface
+
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.Q)
+class SurfaceControlCompat(surface: Surface, debugName: String) {
+ private var mNativeSurfaceControl: Long
+
+ init {
+ mNativeSurfaceControl = nCreateFromWindow(surface, debugName)
+ if (mNativeSurfaceControl == 0L) {
+ throw IllegalArgumentException()
+ }
+ }
+
+ open class Transaction() {
+ private var mNativeSurfaceTransaction: Long
+
+ init {
+ mNativeSurfaceTransaction = nTransactionCreate()
+ if (mNativeSurfaceTransaction == 0L) {
+ throw java.lang.IllegalArgumentException()
+ }
+ }
+
+ fun delete() {
+ if (mNativeSurfaceTransaction != 0L) {
+ nTransactionDelete(mNativeSurfaceTransaction)
+ }
+ mNativeSurfaceTransaction = 0L
+ }
+
+ fun finalize() {
+ delete()
+ }
+
+ private external fun nTransactionCreate(): Long
+ private external fun nTransactionDelete(surfaceTransaction: Long)
+
+ companion object {
+ init {
+ System.loadLibrary("graphics-core")
+ }
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == this) {
+ return true
+ }
+ if ((other == null) or
+ (other?.javaClass != SurfaceControlCompat::class.java)
+ ) {
+ return false
+ }
+
+ other as SurfaceControlCompat
+ if (other.mNativeSurfaceControl == this.mNativeSurfaceControl) {
+ return true
+ }
+
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return mNativeSurfaceControl.hashCode()
+ }
+
+ protected fun finalize() {
+ nRelease(mNativeSurfaceControl)
+ mNativeSurfaceControl = 0
+ }
+
+ private external fun nCreateFromWindow(surface: Surface, debugName: String): Long
+ private external fun nRelease(surfaceControl: Long)
+
+ companion object {
+ init {
+ System.loadLibrary("graphics-core")
+ }
+ }
+}
\ No newline at end of file
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt b/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
index 92db36c..e421a6d 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
@@ -49,6 +49,7 @@
}
}
+ @Suppress("DEPRECATION")
override fun parseResult(resultCode: Int, intent: Intent?): Set<Permission> {
return intent
?.getParcelableArrayListExtra<ProtoPermission>(KEY_GRANTED_PERMISSIONS_JETPACK)
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
index fac68c0..1c14738 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
@@ -60,6 +60,7 @@
private const val EXTRA_PERMISSIONS_GRANTED: String = "hs.data_points_has_permissions"
/** Retrieves the [DataPoint] s that are contained in the given [Intent], if any. */
+ @Suppress("DEPRECATION")
@JvmStatic
@Keep
public fun getDataPoints(intent: Intent): List<DataPoint> =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
index d4912c3..74118ef 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
@@ -105,6 +105,7 @@
* Creates a [PassiveGoal] from an [Intent]. Returns null if no [PassiveGoal] is stored in
* the given intent.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
public fun fromIntent(intent: Intent): PassiveGoal? = intent.getParcelableExtra(EXTRA_KEY)
}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
index c57e698..fd6b262 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
@@ -78,6 +78,7 @@
* Creates a [PassiveMonitoringUpdate] from an [Intent]. Returns null if no
* [PassiveMonitoringUpdate] is stored in the given intent.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
public fun fromIntent(intent: Intent): PassiveMonitoringUpdate? =
intent.getParcelableExtra(EXTRA_KEY)
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index fd65713..af40a5a 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -459,7 +459,11 @@
}
mWidth = mBitmaps[0].getWidth();
mHeight = mBitmaps[0].getHeight();
- retriever.release();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
}
private void cleanupStaleOutputs() {
@@ -702,7 +706,11 @@
assertEquals("Wrong primary index", primary,
Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
- retriever.release();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
if (useGrid) {
MediaExtractor extractor = new MediaExtractor();
diff --git a/libraryversions.toml b/libraryversions.toml
index d321fe6..5e11b1e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,9 +1,9 @@
[versions]
-ACTIVITY = "1.5.0-alpha06"
+ACTIVITY = "1.6.0-alpha04"
ADS_IDENTIFIER = "1.0.0-alpha05"
ANNOTATION = "1.4.0-alpha03"
ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
-APPCOMPAT = "1.5.0-alpha02"
+APPCOMPAT = "1.6.0-alpha04"
APPSEARCH = "1.0.0-alpha05"
ARCH_CORE = "2.2.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha01"
@@ -22,9 +22,9 @@
COMPOSE_MATERIAL3 = "1.0.0-alpha10"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.8.0-alpha08"
-CORE_ANIMATION = "1.0.0-alpha03"
-CORE_ANIMATION_TESTING = "1.0.0-alpha03"
+CORE = "1.9.0-alpha04"
+CORE_ANIMATION = "1.0.0-beta01"
+CORE_ANIMATION_TESTING = "1.0.0-beta01"
CORE_APPDIGEST = "1.0.0-alpha01"
CORE_GOOGLE_SHORTCUTS = "1.1.0-alpha02"
CORE_I18N = "1.0.0-alpha01"
@@ -50,6 +50,7 @@
FUTURES = "1.2.0-alpha01"
GLANCE = "1.0.0-alpha04"
GLANCE_TEMPLATE = "1.0.0-alpha01"
+GRAPHICS = "1.0.0-alpha01"
GRIDLAYOUT = "1.1.0-alpha01"
HEALTH_DATA_CLIENT = "1.0.0-alpha01"
HEALTH_SERVICES_CLIENT = "1.0.0-alpha04"
@@ -113,9 +114,9 @@
TRACING_PERFETTO = "1.0.0-alpha01"
TRANSITION = "1.5.0-alpha01"
TVPROVIDER = "1.1.0-alpha02"
-VECTORDRAWABLE = "1.2.0-alpha03"
-VECTORDRAWABLE_ANIMATED = "1.2.0-alpha01"
-VECTORDRAWABLE_SEEKABLE = "1.0.0-alpha03"
+VECTORDRAWABLE = "1.2.0-beta01"
+VECTORDRAWABLE_ANIMATED = "1.2.0-beta01"
+VECTORDRAWABLE_SEEKABLE = "1.0.0-beta01"
VERSIONED_PARCELABLE = "1.2.0-alpha01"
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.1.0-beta02"
@@ -179,6 +180,7 @@
FRAGMENT = { group = "androidx.fragment", atomicGroupVersion = "versions.FRAGMENT" }
GLANCE = { group = "androidx.glance", atomicGroupVersion = "versions.GLANCE" }
GLANCE_TEMPLATE = { group = "androidx.template", atomicGroupVersion = "versions.GLANCE_TEMPLATE" }
+GRAPHICS = { group = "androidx.graphics", atomicGroupVersion = "versions.GRAPHICS" }
GRIDLAYOUT = { group = "androidx.gridlayout", atomicGroupVersion = "versions.GRIDLAYOUT" }
HEALTH = { group = "androidx.health" }
HEIFWRITER = { group = "androidx.heifwriter", atomicGroupVersion = "versions.HEIFWRITER" }
diff --git a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
index 1cab987..04055db 100644
--- a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
+++ b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
@@ -52,6 +52,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+@SuppressWarnings("deprecation")
@MediumTest
@RunWith(AndroidJUnit4.class)
public class ServiceLifecycleTest {
diff --git a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index 16acd88..717e9c1 100644
--- a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -1185,6 +1185,7 @@
* @param mediaButtonEvent The media button event intent.
* @return True if the event was handled, false otherwise.
*/
+ @SuppressWarnings("deprecation")
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
if (android.os.Build.VERSION.SDK_INT >= 27) {
// Double tap of play/pause as skipping to next is already handled by framework,
diff --git a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
index 10d515c..1b26d16 100644
--- a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
+++ b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
@@ -150,6 +150,7 @@
mMediaBrowser = mediaBrowser;
}
+ @SuppressWarnings("deprecation")
@Override
public void onConnected() {
MediaControllerCompat mediaController = new MediaControllerCompat(mContext,
@@ -186,6 +187,7 @@
* @param intent The intent to parse.
* @return The extracted {@link KeyEvent} if found, or null.
*/
+ @SuppressWarnings("deprecation")
public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
if (mediaSessionCompat == null || intent == null
|| !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
diff --git a/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java b/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
index 8ff807d..f2df815 100644
--- a/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
+++ b/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
@@ -39,6 +39,7 @@
import com.google.common.util.concurrent.ListenableFuture;
+import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.Executor;
@@ -195,7 +196,11 @@
Bitmap musicAlbumBitmap = extractAlbumArt(retriever);
if (retriever != null) {
- retriever.release();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about that...
+ }
}
MediaMetadata metadata = mediaItem.getMetadata();
diff --git a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
index d2b5aa5..d694aa5 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
@@ -826,7 +826,11 @@
retriever.setDataSource(file);
String rotation = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
- retriever.release();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
retriever = null;
assertNotNull(rotation);
assertEquals(Integer.parseInt(rotation), angle);
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 34f5ad0..27ec402 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -1672,6 +1672,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
final class MediaButtonReceiver extends BroadcastReceiver {
+ @SuppressWarnings("deprecation")
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
index 5dc5374..9515408 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
@@ -150,6 +150,7 @@
}
}
+ @SuppressWarnings("deprecation")
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null || intent.getAction() == null) {
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
index b53c19f..602e9f0 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
@@ -184,6 +184,7 @@
assertThat(ids).asList().containsExactly(R.id.nav_root, R.id.second_test)
}
+ @Suppress("DEPRECATION")
@Test
fun generateExplicitStartDestinationWithArgs() {
val deepLinkBuilder = NavDeepLinkBuilder(targetContext)
@@ -204,6 +205,7 @@
assertThat(args).containsExactly("arg1", "arg2").inOrder()
}
+ @Suppress("DEPRECATION")
@Test
fun generateExplicitNavRootWithArgs() {
val deepLinkBuilder = NavDeepLinkBuilder(targetContext)
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 694d2c1..f685e85 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -307,6 +307,7 @@
return ProfileVersion.V010_P;
case Build.VERSION_CODES.S:
+ case Build.VERSION_CODES.S_V2:
return ProfileVersion.V015_S;
default:
@@ -339,6 +340,7 @@
// The profiles for S require a typeIdCount. Therefore metadata is required.
case Build.VERSION_CODES.S:
+ case Build.VERSION_CODES.S_V2:
return true;
default:
diff --git a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
index 14e27aa..5fcd73a 100644
--- a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
+++ b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
@@ -31,8 +31,8 @@
public static List<Long> createLongData(int num) {
List<Long> items = new ArrayList<>(num);
- for (int i = 0; i < num; ++i) {
- items.add(new Long(i));
+ for (long i = 0; i < num; ++i) {
+ items.add(i);
}
return items;
}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
index 77e5126..3ad4b36 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
@@ -28,6 +28,7 @@
* remote route volume in lock screen. It routes media key events back
* to main app activity SampleMediaRouterActivity.
*/
+@SuppressWarnings("deprecation")
public class SampleMediaButtonReceiver extends BroadcastReceiver {
private static final String TAG = "SampleMediaButtonReceiver";
private static SampleMediaRouterActivity mActivity;
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
index e695d7d..afe07ed9 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
@@ -51,6 +51,7 @@
*
* @see SampleMediaRouteProviderService
*/
+@SuppressWarnings("deprecation")
class SampleMediaRouteProvider extends MediaRouteProvider {
private static final String TAG = "SampleMrp";
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index 6f72b20..507d14e 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -78,6 +78,7 @@
* targets.
* </p>
*/
+@SuppressWarnings("deprecation")
public class SampleMediaRouterActivity extends AppCompatActivity {
private static final String TAG = "SampleMediaRouter";
private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment";
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
index 6e2cbb4d..3f218ab 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
@@ -16,6 +16,7 @@
import android.app.Activity;
import android.os.Bundle;
+@SuppressWarnings("deprecation")
public class DetailsActivity extends Activity
{
public static final String EXTRA_ITEM = "item";
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
index f1a5ceb..8930028 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
@@ -20,6 +20,7 @@
import androidx.fragment.app.FragmentActivity;
+@SuppressWarnings("deprecation")
public class DetailsSupportActivity extends FragmentActivity
{
public static final String EXTRA_ITEM = "item";
diff --git a/settings.gradle b/settings.gradle
index 58d4a8c..f25ddc5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -51,8 +51,9 @@
value("BUILD_NUMBER", buildNumber)
link("ci.android.com build", "https://ci.android.com/builds/branches/aosp-androidx-main/grid?head=$buildNumber&tail=$buildNumber")
}
- publishAlways()
- publishIfAuthenticated()
+ // Do not publish scan for androidx-platform-dev
+ // publishAlways()
+ // publishIfAuthenticated()
}
}
@@ -544,6 +545,7 @@
includeProject(":health:health-data-client", [BuildType.MAIN])
includeProject(":health:health-services-client", [BuildType.MAIN])
includeProject(":heifwriter:heifwriter", [BuildType.MAIN])
+includeProject(":graphics:graphics-core", [BuildType.MAIN])
includeProject(":hilt:hilt-common", [BuildType.MAIN])
includeProject(":hilt:hilt-compiler", [BuildType.MAIN])
includeProject(":hilt:hilt-navigation", [BuildType.MAIN, BuildType.COMPOSE])
@@ -796,7 +798,7 @@
includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
-includeProject(":window:window-java", [BuildType.MAIN])
+includeProject(":window:window-java", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":window:window-rxjava2", [BuildType.MAIN])
includeProject(":window:window-rxjava3", [BuildType.MAIN])
includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
diff --git a/text/text/build.gradle b/text/text/build.gradle
index 1fd3a06..adbeebc 100644
--- a/text/text/build.gradle
+++ b/text/text/build.gradle
@@ -25,6 +25,7 @@
dependencies {
implementation(libs.kotlinStdlib)
+ implementation("androidx.core:core:1.7.0")
api "androidx.annotation:annotation:1.2.0"
@@ -32,7 +33,6 @@
testImplementation(libs.testRunner)
testImplementation(libs.junit)
- androidTestImplementation("androidx.core:core:1.5.0-rc02")
androidTestImplementation(project(":compose:ui:ui-test-font"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
index e4a4227..fba9e8f 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
@@ -74,12 +74,42 @@
}
@Test
- fun create_returnsGivenValues() {
+ fun create_returnsGivenValues_includePadding_false() {
val text = "abc"
val paint = TextPaint()
val width = 100
val metrics = BoringLayout.isBoring(text, paint)
- val boringLayout = create(text, paint, width, metrics)
+ val boringLayout = create(
+ text = text,
+ paint = paint,
+ width = width,
+ metrics = metrics,
+ includePadding = false
+ )
+
+ assertThat(boringLayout.text).isEqualTo(text)
+ assertThat(boringLayout.paint).isEqualTo(paint)
+ // The width and height of the boringLayout is the same in metrics, indicating metrics is
+ // passed correctly.
+ assertThat(boringLayout.getLineWidth(0).toInt()).isEqualTo(metrics.width)
+ assertThat(boringLayout.getLineBottom(0) - boringLayout.getLineTop(0))
+ .isEqualTo(metrics.descent - metrics.ascent)
+ assertThat(boringLayout.width).isEqualTo(width)
+ }
+
+ @Test
+ fun create_returnsGivenValues_includePadding_true() {
+ val text = "abc"
+ val paint = TextPaint()
+ val width = 100
+ val metrics = BoringLayout.isBoring(text, paint)
+ val boringLayout = create(
+ text = text,
+ paint = paint,
+ width = width,
+ metrics = metrics,
+ includePadding = true
+ )
assertThat(boringLayout.text).isEqualTo(text)
assertThat(boringLayout.paint).isEqualTo(paint)
@@ -137,7 +167,7 @@
}
@Test
- fun create_defaultIncludePad_isTrue() {
+ fun create_defaultIncludePad_isFalse() {
val text: CharSequence = "abcdefghijk"
val paint = TextPaint()
val metrics = BoringLayout.isBoring(text, paint)
@@ -150,8 +180,8 @@
val topPad = boringLayout.topPadding
val bottomPad = boringLayout.bottomPadding
- // Top and bottom padding are not 0 at the same time, indicating includePad is true.
- assertThat(topPad * topPad + bottomPad * bottomPad).isGreaterThan(0)
+ // Top and bottom padding are 0 at the same time, indicating includePad is false
+ assertThat(topPad + bottomPad).isEqualTo(0)
}
@Test(expected = IllegalArgumentException::class)
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java
new file mode 100644
index 0000000..b1379e8
--- /dev/null
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 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 androidx.compose.ui.text.android;
+
+import android.text.BoringLayout;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Platform BoringLayout constructor has marked TextUtils.TruncateAt as NonNull even though it is
+ * nullable, and have to be nullable.
+ *
+ * This class has the same signature of the BoringLayout constructor with only difference of
+ * ellipsize is marked as Nullable.
+ *
+ * This was the only way to prevent compilation failure for nullability of TruncateAt ellipsize.
+ *
+ * See b/225695033
+ */
+@RequiresApi(33)
+class BoringLayoutConstructor33 {
+
+ private BoringLayoutConstructor33() {}
+
+ @NonNull
+ public static BoringLayout create(
+ @NonNull CharSequence text,
+ @NonNull TextPaint paint,
+ @IntRange(from = 0) int width,
+ @NonNull Layout.Alignment alignment,
+ float lineSpacingMultiplier,
+ float lineSpacingExtra,
+ @NonNull BoringLayout.Metrics metrics,
+ boolean includePadding,
+ @Nullable TextUtils.TruncateAt ellipsize,
+ @IntRange(from = 0) int ellipsizedWidth,
+ boolean useFallbackLineSpacing
+ ) {
+ return new BoringLayout(
+ text,
+ paint,
+ width,
+ alignment,
+ lineSpacingMultiplier,
+ lineSpacingExtra,
+ metrics,
+ includePadding,
+ ellipsize,
+ ellipsizedWidth,
+ useFallbackLineSpacing
+ );
+ }
+}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
index f4300cb..f9116ac 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
@@ -18,9 +18,13 @@
import android.text.BoringLayout
import android.text.BoringLayout.Metrics
import android.text.Layout.Alignment
+import android.text.StaticLayout
import android.text.TextDirectionHeuristic
import android.text.TextPaint
import android.text.TextUtils.TruncateAt
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
/**
* Factory Class for BoringLayout
@@ -28,7 +32,7 @@
@OptIn(InternalPlatformTextApi::class)
internal object BoringLayoutFactory {
/**
- * Try to lay out text by BoringLayout with provided paint and text direction.
+ * Try to layout text by BoringLayout with provided paint and text direction.
*
* @param text the text to analyze.
* @param paint TextPaint which carries text style parameters such as size, weight, font e.g.
@@ -36,15 +40,16 @@
* @return null if not boring; the width, ascent, and descent in a BoringLayout.Metrics
* object.
*/
+ @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
fun measure(
text: CharSequence,
- paint: TextPaint?,
+ paint: TextPaint,
textDir: TextDirectionHeuristic
): Metrics? {
- return if (!textDir.isRtl(text, 0, text.length)) {
- BoringLayout.isBoring(text, paint, null /* metrics */)
+ return if (BuildCompat.isAtLeastT()) {
+ BoringLayoutFactory33.isBoring(text, paint, textDir)
} else {
- null
+ BoringLayoutFactoryDefault.isBoring(text, paint, textDir)
}
}
@@ -58,23 +63,33 @@
* @param alignment To which edge the text is aligned.
* @param includePadding Whether to add extra space beyond font ascent and descent (which is
* needed to avoid clipping in some languages, such as Arabic and Kannada). Default is true.
+ * @param useFallbackLineSpacing Sets Android TextView#setFallbackLineSpacing. This value should
+ * be set to true in most cases and it is the default on platform; otherwise tall scripts such
+ * as Burmese or Tibetan result in clippings on top and bottom sometimes making the text
+ * not-readable.
* @param ellipsize The ellipsize option specifying how the overflowed text is handled.
* @param ellipsizedWidth The width where the exceeding text will be ellipsized, in pixel.
+ *
+ * @see BoringLayout.isFallbackLineSpacingEnabled
+ * @see StaticLayout.Builder.setUseLineSpacingFromFallbacks
**/
+ @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
fun create(
text: CharSequence,
paint: TextPaint,
width: Int,
metrics: Metrics,
alignment: Alignment = Alignment.ALIGN_NORMAL,
- includePadding: Boolean = true,
+ includePadding: Boolean = LayoutCompat.DEFAULT_INCLUDE_PADDING,
+ useFallbackLineSpacing: Boolean = LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
ellipsize: TruncateAt? = null,
- ellipsizedWidth: Int = width
+ ellipsizedWidth: Int = width,
): BoringLayout {
require(width >= 0)
require(ellipsizedWidth >= 0)
- return if (ellipsize == null) {
- BoringLayout(
+
+ return if (BuildCompat.isAtLeastT()) {
+ BoringLayoutFactory33.create(
text,
paint,
width,
@@ -82,10 +97,13 @@
LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER,
LayoutCompat.DEFAULT_LINESPACING_EXTRA,
metrics,
- includePadding
+ includePadding,
+ useFallbackLineSpacing,
+ ellipsize,
+ ellipsizedWidth
)
} else {
- BoringLayout(
+ BoringLayoutFactoryDefault.create(
text,
paint,
width,
@@ -99,4 +117,107 @@
)
}
}
+
+ /**
+ * Returns whether fallbackLineSpacing is enabled for the given layout.
+ */
+ @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun isFallbackLineSpacingEnabled(layout: BoringLayout): Boolean {
+ return if (BuildCompat.isAtLeastT()) {
+ BoringLayoutFactory33.isFallbackLineSpacingEnabled(layout)
+ } else {
+ return false
+ }
+ }
+}
+
+@RequiresApi(33)
+@OptIn(InternalPlatformTextApi::class)
+private object BoringLayoutFactory33 {
+
+ @JvmStatic
+ @DoNotInline
+ fun isBoring(text: CharSequence, paint: TextPaint, textDir: TextDirectionHeuristic): Metrics? {
+ return BoringLayout.isBoring(
+ text,
+ paint,
+ textDir,
+ LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
+ null /* metrics */
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun create(
+ text: CharSequence,
+ paint: TextPaint,
+ width: Int,
+ alignment: Alignment,
+ lineSpacingMultiplier: Float,
+ lineSpacingExtra: Float,
+ metrics: Metrics,
+ includePadding: Boolean,
+ useFallbackLineSpacing: Boolean,
+ ellipsize: TruncateAt? = null,
+ ellipsizedWidth: Int = width
+ ): BoringLayout {
+ return BoringLayoutConstructor33.create(
+ text,
+ paint,
+ width,
+ alignment,
+ lineSpacingMultiplier,
+ lineSpacingExtra,
+ metrics,
+ includePadding,
+ ellipsize,
+ ellipsizedWidth,
+ useFallbackLineSpacing
+ )
+ }
+
+ fun isFallbackLineSpacingEnabled(layout: BoringLayout): Boolean {
+ return layout.isFallbackLineSpacingEnabled
+ }
+}
+
+private object BoringLayoutFactoryDefault {
+ @JvmStatic
+ @DoNotInline
+ fun isBoring(text: CharSequence, paint: TextPaint, textDir: TextDirectionHeuristic): Metrics? {
+ return if (!textDir.isRtl(text, 0, text.length)) {
+ return BoringLayout.isBoring(text, paint, null /* metrics */)
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun create(
+ text: CharSequence,
+ paint: TextPaint,
+ width: Int,
+ alignment: Alignment,
+ lineSpacingMultiplier: Float,
+ lineSpacingExtra: Float,
+ metrics: Metrics,
+ includePadding: Boolean,
+ ellipsize: TruncateAt? = null,
+ ellipsizedWidth: Int = width
+ ): BoringLayout {
+ return BoringLayout(
+ text,
+ paint,
+ width,
+ alignment,
+ lineSpacingMultiplier,
+ lineSpacingExtra,
+ metrics,
+ includePadding,
+ ellipsize,
+ ellipsizedWidth
+ )
+ }
}
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
index ef17dfb..e2c9747 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
@@ -30,6 +30,7 @@
import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
+import androidx.core.os.BuildCompat
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
@@ -41,7 +42,7 @@
private val delegate: StaticLayoutFactoryImpl = if (Build.VERSION.SDK_INT >= 23) {
StaticLayoutFactory23()
} else {
- StaticLayoutFactoryPre21()
+ StaticLayoutFactoryDefault()
}
/**
@@ -98,6 +99,20 @@
)
)
}
+
+ /**
+ * Returns whether fallbackLineSpacing is enabled for the given layout.
+ *
+ * @param layout StaticLayout instance
+ * @param useFallbackLineSpacing fallbackLineSpacing canfiguration passed while creating the
+ * StaticLayout.
+ */
+ fun isFallbackLineSpacingEnabled(
+ layout: StaticLayout,
+ useFallbackLineSpacing: Boolean
+ ): Boolean {
+ return delegate.isFallbackLineSpacingEnabled(layout, useFallbackLineSpacing)
+ }
}
@OptIn(InternalPlatformTextApi::class)
@@ -136,6 +151,8 @@
@DoNotInline // API level specific, do not inline to prevent ART class verification breakages
fun create(params: StaticLayoutParams): StaticLayout
+
+ fun isFallbackLineSpacingEnabled(layout: StaticLayout, useFallbackLineSpacing: Boolean): Boolean
}
@RequiresApi(23)
@@ -166,10 +183,25 @@
}
}.build()
}
+
+ @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ override fun isFallbackLineSpacingEnabled(
+ layout: StaticLayout,
+ useFallbackLineSpacing: Boolean
+ ): Boolean {
+ return if (BuildCompat.isAtLeastT()) {
+ StaticLayoutFactory33.isFallbackLineSpacingEnabled(layout)
+ } else if (Build.VERSION.SDK_INT >= 28) {
+ useFallbackLineSpacing
+ } else {
+ false
+ }
+ }
}
@RequiresApi(26)
private object StaticLayoutFactory26 {
+ @JvmStatic
@DoNotInline
fun setJustificationMode(builder: Builder, justificationMode: Int) {
builder.setJustificationMode(justificationMode)
@@ -178,13 +210,23 @@
@RequiresApi(28)
private object StaticLayoutFactory28 {
+ @JvmStatic
@DoNotInline
fun setUseLineSpacingFromFallbacks(builder: Builder, useFallbackLineSpacing: Boolean) {
builder.setUseLineSpacingFromFallbacks(useFallbackLineSpacing)
}
}
-private class StaticLayoutFactoryPre21 : StaticLayoutFactoryImpl {
+@RequiresApi(33)
+private object StaticLayoutFactory33 {
+ @JvmStatic
+ @DoNotInline
+ fun isFallbackLineSpacingEnabled(layout: StaticLayout): Boolean {
+ return layout.isFallbackLineSpacingEnabled
+ }
+}
+
+private class StaticLayoutFactoryDefault : StaticLayoutFactoryImpl {
companion object {
private var isInitialized = false
@@ -274,4 +316,11 @@
params.ellipsizedWidth
)
}
+
+ override fun isFallbackLineSpacingEnabled(
+ layout: StaticLayout,
+ useFallbackLineSpacing: Boolean
+ ): Boolean {
+ return false
+ }
}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 938b04b..ad49528 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -17,9 +17,10 @@
import android.graphics.Canvas
import android.graphics.Path
-import android.os.Build
+import android.text.BoringLayout
import android.text.Layout
import android.text.Spanned
+import android.text.StaticLayout
import android.text.TextDirectionHeuristic
import android.text.TextDirectionHeuristics
import android.text.TextPaint
@@ -68,7 +69,11 @@
* @param lineSpacingMultiplier the multiplier to be applied to each line of the text.
* @param lineSpacingExtra the extra height to be added to each line of the text.
* @param includePadding defines whether the extra space to be applied beyond font ascent and
- * descent,
+ * descent
+ * @param fallbackLineSpacing Sets Android TextView#setFallbackLineSpacing. This value should
+ * be set to true in most cases and it is the default on platform; otherwise tall scripts such
+ * as Burmese or Tibetan result in clippings on top and bottom sometimes making the text
+ * not-readable.
* @param maxLines the maximum number of lines to be laid out.
* @param breakStrategy the strategy to be used for line breaking
* @param hyphenationFrequency set the frequency to control the amount of automatic hyphenation
@@ -81,7 +86,9 @@
* element in the array is applied to the corresponding line. For lines past the last element in
* array, the last element repeats.
* @param layoutIntrinsics previously calculated [LayoutIntrinsics] for this text
+ *
* @see StaticLayoutFactory
+ * @see BoringLayoutFactory
*
* @suppress
*/
@@ -174,6 +181,7 @@
metrics = boringMetrics,
alignment = frameworkAlignment,
includePadding = includePadding,
+ useFallbackLineSpacing = fallbackLineSpacing,
ellipsize = ellipsize,
ellipsizedWidth = widthInt
)
@@ -439,7 +447,14 @@
}
internal fun isFallbackLinespacingApplied(): Boolean {
- return fallbackLineSpacing && !isBoringLayout && Build.VERSION.SDK_INT >= 28
+ return if (isBoringLayout) {
+ BoringLayoutFactory.isFallbackLineSpacingEnabled(layout as BoringLayout)
+ } else {
+ StaticLayoutFactory.isFallbackLineSpacingEnabled(
+ layout as StaticLayout,
+ fallbackLineSpacing
+ )
+ }
}
}
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index 0fc43fc..5d2cdbb 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -1114,6 +1114,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
public fun launchComplicationDataSourceChooser() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 70046c3..a6b65d1 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -276,6 +276,7 @@
}
// Used by tests.
+ @Suppress("DEPRECATION")
@Throws(TimeoutCancellationException::class)
internal suspend fun createOnWatchEditorSessionImpl(
activity: ComponentActivity,
@@ -1072,6 +1073,7 @@
return intent
}
+ @Suppress("DEPRECATION")
override fun parseResult(resultCode: Int, intent: Intent?) = intent?.let {
val extras = intent.extras?.let { extras ->
Bundle(extras).apply { remove(EXTRA_PROVIDER_INFO) }
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index a266048..4ece658 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -138,6 +138,7 @@
* if there is one or `null` otherwise. Intended for use by the watch face editor activity.
* @throws [TimeoutCancellationException] in case of en error.
*/
+ @Suppress("DEPRECATION")
@SuppressLint("NewApi")
@JvmStatic
@Throws(TimeoutCancellationException::class)
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
index 2294c89..f3218ce 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
@@ -314,6 +314,7 @@
}
/** The watch face component name encoded in the intent. */
+@Suppress("DEPRECATION")
private val Intent.watchFaceComponentName
get() = getParcelableExtra<ComponentName>(
ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
index b5853f9..0cac46c 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
@@ -183,6 +183,7 @@
}
@Override
+ @SuppressWarnings("deprecation")
public void launchComplicationDeniedActivity() {
Intent complicationDeniedIntent =
mActivity.getIntent().getParcelableExtra(
@@ -222,6 +223,7 @@
start(true);
}
+ @SuppressWarnings("deprecation")
void start(boolean shouldShowRequestPermissionRationale) {
if (shouldShowRequestPermissionRationale
&& mDelegate.shouldShowRequestPermissionRationale()) {
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
index 4d8d6fc..87ded0e 100644
--- a/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
+++ b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
@@ -100,6 +100,7 @@
* @return The array of [RemoteInput] previously added with [putRemoteInputsExtra] or null
* which means no user input required.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
@Nullable
public fun getRemoteInputsExtra(intent: Intent): List<RemoteInput>? =
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
index 47c4577..3c9f8ba 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
@@ -97,6 +97,7 @@
* @param intent The intent holding configuration.
* @return The remote intent, or null if none was set.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
public fun getTargetIntent(intent: Intent): Intent? =
intent.getParcelableExtra(EXTRA_INTENT)
@@ -117,6 +118,7 @@
* @param intent The intent holding configuration.
* @return The result receiver, or null if none was set.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
internal fun getRemoteIntentResultReceiver(intent: Intent): ResultReceiver? =
intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
index 99aa404..54eefab 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
@@ -85,6 +85,7 @@
* @return the value of an item previously added with [putWatchFaceComponentExtra], or
* null if no value was found.
*/
+ @Suppress("DEPRECATION")
@JvmStatic
@Nullable
public fun getWatchFaceComponentExtra(watchFaceIntent: Intent): ComponentName? =
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6a28e00..ad88a4a 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -7,6 +7,7 @@
public interface WindowExtensions {
method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
+ method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -16,6 +17,22 @@
}
+package androidx.window.extensions.area {
+
+ @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface WindowAreaComponent {
+ method public void addRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+ method public void endRearDisplaySession();
+ method public void removeRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+ method public void startRearDisplaySession(android.app.Activity, java.util.function.Consumer<java.lang.Integer!>);
+ field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_AVAILABLE = 2; // 0x2
+ field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+ field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+ }
+
+}
+
package androidx.window.extensions.embedding {
@androidx.window.extensions.ExperimentalWindowExtensionsApi public interface ActivityEmbeddingComponent {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 6ac25bd..ca3a882 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -17,6 +17,7 @@
package androidx.window.extensions;
import androidx.annotation.Nullable;
+import androidx.window.extensions.area.WindowAreaComponent;
import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
import androidx.window.extensions.layout.WindowLayoutComponent;
@@ -60,4 +61,14 @@
@Nullable
@ExperimentalWindowExtensionsApi
ActivityEmbeddingComponent getActivityEmbeddingComponent();
+
+ /**
+ * Returns the OEM implementation of {@link WindowAreaComponent} if it is supported on
+ * the device, {@code null} otherwise. The implementation must match the API level reported in
+ * {@link WindowExtensions}.
+ * @return the OEM implementation of {@link WindowAreaComponent}
+ */
+ @Nullable
+ @ExperimentalWindowExtensionsApi
+ WindowAreaComponent getWindowAreaComponent();
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
new file mode 100644
index 0000000..a565eda
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 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 androidx.window.extensions.area;
+
+import android.app.Activity;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.ExperimentalWindowExtensionsApi;
+import androidx.window.extensions.WindowExtensions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Consumer;
+
+/**
+ * The interface definition that will be used by the WindowManager library to get custom
+ * OEM-provided behavior around moving windows between displays or display areas on a device.
+ *
+ * Currently the only behavior supported is RearDisplay Mode, where the window
+ * is moved to the display that faces the same direction as the rear camera.
+ *
+ * <p>This interface should be implemented by OEM and deployed to the target devices.
+ * @see WindowExtensions#getWindowLayoutComponent()
+ */
+@ExperimentalWindowExtensionsApi
+public interface WindowAreaComponent {
+
+ /**
+ * WindowArea status constant to signify that the feature is
+ * unsupported on this device. Could be due to the device not supporting that
+ * specific feature.
+ */
+ int STATUS_UNSUPPORTED = 0;
+
+ /**
+ * WindowArea status constant to signify that the feature is
+ * currently unavailable but is supported on this device. This value could signify
+ * that the current device state does not support the specific feature or another
+ * process is currently enabled in that feature.
+ */
+ int STATUS_UNAVAILABLE = 1;
+
+ /**
+ * WindowArea status constant to signify that the feature is
+ * available to be entered or enabled.
+ */
+ int STATUS_AVAILABLE = 2;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATUS_UNSUPPORTED,
+ STATUS_UNAVAILABLE,
+ STATUS_AVAILABLE
+ })
+ @interface WindowAreaStatus {}
+
+ /**
+ * Session state constant to represent there being no active session
+ * currently in progress. Used by the library to call the correct callbacks if
+ * a session is ended.
+ */
+ int SESSION_STATE_INACTIVE = 0;
+
+ /**
+ * Session state constant to represent that there is an
+ * active session currently in progress. Used by the library to
+ * know when to return the session object to the developer when the
+ * session is created and active.
+ */
+ int SESSION_STATE_ACTIVE = 1;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_STATE_ACTIVE,
+ SESSION_STATE_INACTIVE
+ })
+ @interface WindowAreaSessionState {}
+
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, the library will post the result of the listener on the executor
+ * provided by the developer.
+ *
+ * The listener provided will receive values that
+ * correspond to the [WindowAreaStatus] value that aligns with the current status
+ * of the rear display.
+ * @param consumer interested in receiving updates to WindowAreaStatus.
+ */
+ void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to WindowAreaStatus
+ */
+ void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+
+ /**
+ * Creates and starts a rear display session and sends state updates to the
+ * consumer provided. This consumer will receive a constant represented by
+ * [WindowAreaSessionState] to represent the state of the current rear display
+ * session. We will translate to a more friendly interface in the library.
+ *
+ * Because this is being called from the OEM provided extensions, the library
+ * will post the result of the listener on the executor provided by the developer.
+ *
+ * @param activity to allow that the OEM implementation will use as a base
+ * context and to identify the source display area of the request.
+ * The reference to the activity instance must not be stored in the OEM
+ * implementation to prevent memory leaks.
+ * @param consumer to provide updates to the client on the status of the session
+ * @throws UnsupportedOperationException if this method is called when RearDisplay
+ * mode is not available. This could be to an incompatible device state or when
+ * another process is currently in this mode.
+ */
+ @SuppressWarnings("ExecutorRegistration") // Jetpack will post it on the app-provided executor.
+ void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull Consumer<Integer> consumer);
+
+ /**
+ * Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
+ * provided in the {@code startRearDisplaySession} method. This method is only
+ * called through the {@code RearDisplaySession} provided to the developer.
+ */
+ void endRearDisplaySession();
+}
diff --git a/window/window-java/api/public_plus_experimental_current.txt b/window/window-java/api/public_plus_experimental_current.txt
index 709904b..a56988b 100644
--- a/window/window-java/api/public_plus_experimental_current.txt
+++ b/window/window-java/api/public_plus_experimental_current.txt
@@ -1,4 +1,15 @@
// Signature format: 4.0
+package androidx.window.java.area {
+
+ @androidx.window.core.ExperimentalWindowApi public final class WindowAreaControllerJavaAdapter implements androidx.window.area.WindowAreaController {
+ ctor public WindowAreaControllerJavaAdapter(androidx.window.area.WindowAreaController controller);
+ method public void addRearDisplayStatusListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+ method public void removeRearDisplayStatusListener(androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+ method public void startRearDisplayModeSession(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+ }
+
+}
+
package androidx.window.java.layout {
public final class WindowInfoTrackerCallbackAdapter implements androidx.window.layout.WindowInfoTracker {
diff --git a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
new file mode 100644
index 0000000..584445f
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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 androidx.window.java.area
+
+import android.app.Activity
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaStatus
+import androidx.window.area.WindowAreaController
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * An adapted interface for [WindowAreaController] that provides the information and
+ * functionality around RearDisplay Mode via a callback shaped API.
+ */
+@ExperimentalWindowApi
+class WindowAreaControllerJavaAdapter(
+ private val controller: WindowAreaController
+) : WindowAreaController by controller {
+
+ /**
+ * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
+ */
+ private val lock = ReentrantLock()
+ private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
+
+ /**
+ * Registers a listener to consume [WindowAreaStatus] values defined as
+ * [WindowAreaStatus.UNSUPPORTED], [WindowAreaStatus.UNAVAILABLE], and
+ * [WindowAreaStatus.AVAILABLE]. The values provided through this listener should be used
+ * to determine if you are able to enable rear display Mode at that time. You can use these
+ * values to modify your UI to show/hide controls and determine when to enable features
+ * that use rear display Mode. You should only try and enter rear display mode when your
+ * [consumer] is provided a value of [WindowAreaStatus.AVAILABLE].
+ *
+ * The [consumer] will be provided an initial value on registration, as well as any change
+ * to the status as they occur. This could happen due to hardware device state changes, or if
+ * another process has enabled RearDisplay Mode.
+ *
+ * @see WindowAreaController.rearDisplayStatus
+ */
+ fun addRearDisplayStatusListener(
+ executor: Executor,
+ consumer: Consumer<WindowAreaStatus>
+ ) {
+ val statusFlow = controller.rearDisplayStatus()
+ lock.withLock {
+ if (consumerToJobMap[consumer] == null) {
+ val scope = CoroutineScope(executor.asCoroutineDispatcher())
+ consumerToJobMap[consumer] = scope.launch {
+ statusFlow.collect { consumer.accept(it) }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a listener of [WindowAreaStatus] values
+ * @see WindowAreaController.rearDisplayStatus
+ */
+ fun removeRearDisplayStatusListener(consumer: Consumer<WindowAreaStatus>) {
+ lock.withLock {
+ consumerToJobMap[consumer]?.cancel()
+ consumerToJobMap.remove(consumer)
+ }
+ }
+
+ /**
+ * Starts a RearDisplay Mode session and provides updates through the
+ * [WindowAreaSessionCallback] provided. Due to the nature of moving your Activity to a
+ * different display, your Activity will likely go through a configuration change. Because of
+ * this, if your Activity does not override configuration changes, this method should be called
+ * from a component that outlives the Activity lifecycle such as a
+ * [androidx.lifecycle.ViewModel]. If your Activity does override
+ * configuration changes, it is safe to call this method inside your Activity.
+ *
+ * This method should only be called if you have received a [WindowAreaStatus.AVAILABLE]
+ * value from the listener provided through the [addRearDisplayStatusListener] method. If
+ * you try and enable RearDisplay mode without it being available, you will receive an
+ * [UnsupportedOperationException].
+ *
+ * The [windowAreaSessionCallback] provided will receive a call to
+ * [WindowAreaSessionCallback.onSessionStarted] after your Activity has been moved to the
+ * display corresponding to this mode. RearDisplay mode will stay active until the session
+ * provided through [WindowAreaSessionCallback.onSessionStarted] is closed, or there is a device
+ * state change that makes RearDisplay mode incompatible such as if the device is closed so the
+ * outer-display is no longer in line with the rear camera. When this occurs,
+ * [WindowAreaSessionCallback.onSessionEnded] is called to notify you the session has been
+ * ended.
+ *
+ * @see addRearDisplayStatusListener
+ * @throws UnsupportedOperationException if you try and start a RearDisplay session when
+ * your [WindowAreaController.rearDisplayStatus] does not return a value of
+ * [WindowAreaStatus.AVAILABLE]
+ */
+ fun startRearDisplayModeSession(
+ activity: Activity,
+ executor: Executor,
+ windowAreaSessionCallback: WindowAreaSessionCallback
+ ) {
+ controller.rearDisplayMode(activity, executor, windowAreaSessionCallback)
+ }
+}
\ No newline at end of file
diff --git a/window/window-samples/build.gradle b/window/window-samples/build.gradle
index 1b3060c1..e66802d 100644
--- a/window/window-samples/build.gradle
+++ b/window/window-samples/build.gradle
@@ -46,7 +46,7 @@
implementation "androidx.browser:browser:1.3.0"
implementation("androidx.startup:startup-runtime:1.1.0")
- implementation(project(":window:window"))
+ implementation(project(":window:window-java"))
debugImplementation(libs.leakcanary)
androidTestImplementation(libs.testCore)
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index de10c6a..76ad8e2 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -47,6 +47,16 @@
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|screenSize"
android:label="@string/window_metrics"/>
+ <activity android:name=".RearDisplayActivityConfigChanges"
+ android:exported="true"
+ android:configChanges=
+ "orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+ android:label="@string/rear_display">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
<activity
android:name=".embedding.SplitActivityA"
android:exported="true"
diff --git a/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt b/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt
new file mode 100644
index 0000000..f7e839f
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 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 androidx.window.sample
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaSession
+import androidx.window.area.WindowAreaStatus
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.java.area.WindowAreaControllerJavaAdapter
+import androidx.window.sample.databinding.ActivityRearDisplayBinding
+import androidx.window.sample.infolog.InfoLogAdapter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.Executor
+
+/**
+ * Demo Activity that showcases listening for RearDisplay Status
+ * as well as enabling/disabling RearDisplay mode. This Activity
+ * implements [WindowAreaSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
+
+ private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
+ private var rearDisplaySession: WindowAreaSession? = null
+ private val infoLogAdapter = InfoLogAdapter()
+ private lateinit var binding: ActivityRearDisplayBinding
+ private lateinit var executor: Executor
+
+ private val rearDisplayStatusListener = Consumer<WindowAreaStatus> { status ->
+ infoLogAdapter.append(getCurrentTimeString(), status.toString())
+ infoLogAdapter.notifyDataSetChanged()
+ updateRearDisplayButton(status)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityRearDisplayBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ executor = ContextCompat.getMainExecutor(this)
+ windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())
+
+ binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+ binding.rearDisplayButton.setOnClickListener {
+ if (rearDisplaySession != null) {
+ rearDisplaySession?.close()
+ } else {
+ windowAreaController.startRearDisplayModeSession(
+ this,
+ executor,
+ this)
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ windowAreaController.addRearDisplayStatusListener(
+ executor,
+ rearDisplayStatusListener
+ )
+ }
+
+ override fun onStop() {
+ super.onStop()
+ windowAreaController.removeRearDisplayStatusListener(rearDisplayStatusListener)
+ }
+
+ override fun onSessionStarted(session: WindowAreaSession) {
+ rearDisplaySession = session
+ infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
+ infoLogAdapter.notifyDataSetChanged()
+ }
+
+ override fun onSessionEnded() {
+ rearDisplaySession = null
+ infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
+ infoLogAdapter.notifyDataSetChanged()
+ }
+
+ private fun updateRearDisplayButton(status: WindowAreaStatus) {
+ if (rearDisplaySession != null) {
+ binding.rearDisplayButton.isEnabled = true
+ binding.rearDisplayButton.text = "Disable RearDisplay Mode"
+ return
+ }
+ when (status) {
+ WindowAreaStatus.UNSUPPORTED -> {
+ binding.rearDisplayButton.isEnabled = false
+ binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+ }
+ WindowAreaStatus.UNAVAILABLE -> {
+ binding.rearDisplayButton.isEnabled = false
+ binding.rearDisplayButton.text = "RearDisplay is not currently available"
+ }
+ WindowAreaStatus.AVAILABLE -> {
+ binding.rearDisplayButton.isEnabled = true
+ binding.rearDisplayButton.text = "Enable RearDisplay Mode"
+ }
+ }
+ }
+
+ private fun getCurrentTimeString(): String {
+ val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+ val currentDate = sdf.format(Date())
+ return currentDate.toString()
+ }
+
+ private companion object {
+ private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
+ }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_rear_display.xml b/window/window-samples/src/main/res/layout/activity_rear_display.xml
new file mode 100644
index 0000000..43bea60
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/activity_rear_display.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 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.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/rearStatusRecyclerView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <Button
+ android:id="@+id/rear_display_button"
+ android:text="Enable RearDisplay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:layout_marginBottom="32dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index 5faaae6..25d5eef 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -51,4 +51,6 @@
<string name="occlusion_is_none">Occlusion is none</string>
<string name="window_metrics">Window metrics</string>
<string name="window_metrics_description">Demo of using WindowMetrics API with activity handling rotations.</string>
+ <string name="rear_display">Rear Display Mode</string>
+ <string name="rear_display_description">Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode</string>
</resources>
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index e166e4b..3f1b25c 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -1,4 +1,36 @@
// Signature format: 4.0
+package androidx.window.area {
+
+ @androidx.window.core.ExperimentalWindowApi public interface WindowAreaController {
+ method public default static androidx.window.area.WindowAreaController getOrCreate();
+ field public static final androidx.window.area.WindowAreaController.Companion Companion;
+ }
+
+ public static final class WindowAreaController.Companion {
+ method public androidx.window.area.WindowAreaController getOrCreate();
+ }
+
+ @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSession {
+ method public void close();
+ }
+
+ @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionCallback {
+ method public void onSessionEnded();
+ method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+ }
+
+ @androidx.window.core.ExperimentalWindowApi public final class WindowAreaStatus {
+ field public static final androidx.window.area.WindowAreaStatus AVAILABLE;
+ field public static final androidx.window.area.WindowAreaStatus.Companion Companion;
+ field public static final androidx.window.area.WindowAreaStatus UNAVAILABLE;
+ field public static final androidx.window.area.WindowAreaStatus UNSUPPORTED;
+ }
+
+ public static final class WindowAreaStatus.Companion {
+ }
+
+}
+
package androidx.window.core {
@kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWindowApi {
diff --git a/window/window/build.gradle b/window/window/build.gradle
index ca84543..98b94b3 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -64,6 +64,7 @@
testImplementation(compileOnly(project(":window:extensions:extensions")))
androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.kotlinTestJunit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
new file mode 100644
index 0000000..dad5e14
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.window.TestActivity
+import androidx.window.TestConsumer
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import java.util.function.Consumer
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Ignore
+
+@Ignore // todo(b/222407443)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+class WindowAreaControllerImplTest {
+
+ @get:Rule
+ public val activityScenario: ActivityScenarioRule<TestActivity> =
+ ActivityScenarioRule(TestActivity::class.java)
+
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Test
+ public fun testRearDisplayStatus(): Unit = testScope.runTest {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+ activityScenario.scenario.onActivity {
+ val extensionComponent = FakeWindowAreaComponent()
+ val repo = WindowAreaControllerImpl(extensionComponent)
+ val collector = TestConsumer<WindowAreaStatus>()
+ extensionComponent
+ .updateStatusListeners(WindowAreaComponent.STATUS_UNAVAILABLE)
+ testScope.launch {
+ repo.rearDisplayStatus().collect(collector::accept)
+ }
+ collector.assertValue(WindowAreaStatus.UNAVAILABLE)
+ extensionComponent
+ .updateStatusListeners(WindowAreaComponent.STATUS_AVAILABLE)
+ collector.assertValues(
+ WindowAreaStatus.UNAVAILABLE,
+ WindowAreaStatus.AVAILABLE
+ )
+ }
+ }
+
+ @Test
+ public fun testRearDisplayStatusNullComponent(): Unit = testScope.runTest {
+ activityScenario.scenario.onActivity {
+ val repo = EmptyWindowAreaControllerImpl()
+ val collector = TestConsumer<WindowAreaStatus>()
+ testScope.launch {
+ repo.rearDisplayStatus().collect(collector::accept)
+ }
+ collector.assertValue(WindowAreaStatus.UNSUPPORTED)
+ }
+ }
+
+ /**
+ * Tests the rear display mode flow works as expected. Tests the flow
+ * through WindowAreaControllerImpl with a fake extension. This fake extension
+ * changes the orientation of the activity to landscape when rear display mode is enabled
+ * and then returns it back to portrait when it's disabled.
+ */
+ @TargetApi(Build.VERSION_CODES.N)
+ @Test
+ public fun testRearDisplayMode(): Unit = testScope.runTest {
+ val extensions = FakeWindowAreaComponent()
+ val repo = WindowAreaControllerImpl(extensions)
+ extensions.currentStatus = WindowAreaComponent.STATUS_AVAILABLE
+ val callback = TestWindowAreaSessionCallback()
+ activityScenario.scenario.onActivity { testActivity ->
+ testActivity.resetLayoutCounter()
+ testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ testActivity.waitForLayout()
+ }
+
+ activityScenario.scenario.onActivity { testActivity ->
+ assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+ testActivity.resetLayoutCounter()
+ repo.rearDisplayMode(testActivity, Runnable::run, callback)
+ }
+
+ activityScenario.scenario.onActivity { testActivity ->
+ assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+ assert(callback.currentSession != null)
+ testActivity.resetLayoutCounter()
+ callback.endSession()
+ }
+ activityScenario.scenario.onActivity { testActivity ->
+ assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+ assert(callback.currentSession == null)
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Test
+ public fun testRearDisplayModeReturnsError(): Unit = testScope.runTest {
+ val extensionComponent = FakeWindowAreaComponent()
+ extensionComponent.currentStatus = WindowAreaComponent.STATUS_UNAVAILABLE
+ val repo = WindowAreaControllerImpl(extensionComponent)
+ val callback = TestWindowAreaSessionCallback()
+ activityScenario.scenario.onActivity { testActivity ->
+ assertFailsWith(
+ exceptionClass = UnsupportedOperationException::class,
+ block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+ )
+ }
+ }
+
+ @Test
+ public fun testRearDisplayModeNullComponent(): Unit = testScope.runTest {
+ val repo = EmptyWindowAreaControllerImpl()
+ val callback = TestWindowAreaSessionCallback()
+ activityScenario.scenario.onActivity { testActivity ->
+ assertFailsWith(
+ exceptionClass = UnsupportedOperationException::class,
+ block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+ )
+ }
+ }
+
+ private class FakeWindowAreaComponent : WindowAreaComponent {
+ val statusListeners = mutableListOf<Consumer<Int>>()
+ var currentStatus = WindowAreaComponent.STATUS_UNSUPPORTED
+ var testActivity: Activity? = null
+ var sessionConsumer: Consumer<Int>? = null
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ statusListeners.add(consumer)
+ consumer.accept(currentStatus)
+ }
+
+ override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ statusListeners.remove(consumer)
+ }
+
+ // Fake WindowAreaComponent will change the orientation of the activity to signal
+ // entering rear display mode, as well as ending the session
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun startRearDisplaySession(
+ activity: Activity,
+ rearDisplaySessionConsumer: Consumer<Int>
+ ) {
+ if (currentStatus != WindowAreaComponent.STATUS_AVAILABLE) {
+ throw WindowAreaController.REAR_DISPLAY_ERROR
+ }
+ testActivity = activity
+ sessionConsumer = rearDisplaySessionConsumer
+ testActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ rearDisplaySessionConsumer.accept(WindowAreaComponent.SESSION_STATE_ACTIVE)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun endRearDisplaySession() {
+ testActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ sessionConsumer?.accept(WindowAreaComponent.SESSION_STATE_INACTIVE)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun updateStatusListeners(newStatus: Int) {
+ currentStatus = newStatus
+ for (consumer in statusListeners) {
+ consumer.accept(currentStatus)
+ }
+ }
+ }
+
+ private class TestWindowAreaSessionCallback : WindowAreaSessionCallback {
+
+ var currentSession: WindowAreaSession? = null
+ var error: Throwable? = null
+
+ override fun onSessionStarted(session: WindowAreaSession) {
+ currentSession = session
+ }
+
+ override fun onSessionEnded() {
+ currentSession = null
+ }
+
+ fun endSession() = currentSession?.close()
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
new file mode 100644
index 0000000..996c7ad
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import android.app.Activity
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import java.util.concurrent.Executor
+
+/**
+ * Empty Implementation for devices that do not
+ * support the [WindowAreaController] functionality
+ */
+@ExperimentalWindowApi
+internal class EmptyWindowAreaControllerImpl : WindowAreaController {
+ override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+ return flowOf(WindowAreaStatus.UNSUPPORTED)
+ }
+
+ override fun rearDisplayMode(
+ activity: Activity,
+ executor: Executor,
+ windowAreaSessionCallback: WindowAreaSessionCallback
+ ) {
+ throw WindowAreaController.REAR_DISPLAY_ERROR
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
new file mode 100644
index 0000000..9a5bbd3
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+@ExperimentalWindowApi
+internal class RearDisplaySessionImpl(
+ private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaSession {
+
+ override fun close() {
+ windowAreaComponent.endRearDisplaySession()
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
new file mode 100644
index 0000000..05aa96e
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.window.core.BuildConfig
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.SpecificationComputer
+import androidx.window.extensions.WindowExtensionsProvider
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.flow.Flow
+import java.util.concurrent.Executor
+import kotlin.jvm.Throws
+
+/**
+ * An interface to provide the information and behavior around moving windows between
+ * displays or display areas on a device.
+ */
+@ExperimentalWindowApi
+interface WindowAreaController {
+
+ /*
+ Marked with RestrictTo as we iterate and define the
+ Kotlin API we want to provide.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun rearDisplayStatus(): Flow<WindowAreaStatus>
+
+ /*
+ Marked with RestrictTo as we iterate and define the
+ Kotlin API we want to provide.
+ */
+ @Throws(UnsupportedOperationException::class)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun rearDisplayMode(
+ activity: Activity,
+ executor: Executor,
+ windowAreaSessionCallback: WindowAreaSessionCallback
+ )
+
+ public companion object {
+ internal val REAR_DISPLAY_ERROR =
+ UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+
+ private val TAG = WindowAreaController::class.simpleName
+
+ private var decorator: WindowAreaControllerDecorator = EmptyDecorator
+
+ /**
+ * Provides an instance of [WindowAreaController].
+ */
+ @JvmName("getOrCreate")
+ @JvmStatic
+ public fun getOrCreate(): WindowAreaController {
+ var windowAreaComponentExtensions: WindowAreaComponent?
+ try {
+ windowAreaComponentExtensions = WindowExtensionsProvider
+ .getWindowExtensions()
+ .windowAreaComponent
+ } catch (t: Throwable) {
+ if (BuildConfig.verificationMode == SpecificationComputer.VerificationMode.STRICT) {
+ Log.d(TAG, "Failed to load WindowExtensions")
+ }
+ windowAreaComponentExtensions = null
+ }
+ val controller =
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+ windowAreaComponentExtensions == null) {
+ EmptyWindowAreaControllerImpl()
+ } else {
+ WindowAreaControllerImpl(windowAreaComponentExtensions)
+ }
+ return decorator.decorate(controller)
+ }
+
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun overrideDecorator(overridingDecorator: WindowAreaControllerDecorator) {
+ decorator = overridingDecorator
+ }
+
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun reset() {
+ decorator = EmptyDecorator
+ }
+ }
+}
+
+/**
+ * Decorator that allows us to provide different functionality
+ * in our window-testing artifact.
+ */
+@ExperimentalWindowApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface WindowAreaControllerDecorator {
+ /**
+ * Returns an instance of [WindowAreaController] associated to the [Activity]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun decorate(controller: WindowAreaController): WindowAreaController
+}
+
+@ExperimentalWindowApi
+private object EmptyDecorator : WindowAreaControllerDecorator {
+ override fun decorate(controller: WindowAreaController): WindowAreaController {
+ return controller
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
new file mode 100644
index 0000000..0e156818
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.window.core.BuildConfig
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.SpecificationComputer
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+/**
+ * Implementation of WindowAreaController for devices
+ * that do implement the WindowAreaComponent on device.
+ *
+ * Requires [Build.VERSION_CODES.N] due to the use of [Consumer].
+ * Will not be created though on API levels lower than
+ * [Build.VERSION_CODES.S] as that's the min level of support for
+ * this functionality.
+ */
+@ExperimentalWindowApi
+@RequiresApi(Build.VERSION_CODES.N)
+internal class WindowAreaControllerImpl(
+ private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaController {
+
+ private lateinit var rearDisplaySessionConsumer: Consumer<Int>
+ private var currentStatus: WindowAreaStatus? = null
+
+ override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+ return flow {
+ val channel = Channel<WindowAreaStatus>(
+ capacity = BUFFER_CAPACITY,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val listener = Consumer<Int> { status ->
+ currentStatus = WindowAreaStatus.translate(status)
+ channel.trySend(currentStatus ?: WindowAreaStatus.UNSUPPORTED)
+ }
+ windowAreaComponent.addRearDisplayStatusListener(listener)
+ try {
+ for (item in channel) {
+ emit(item)
+ }
+ } finally {
+ windowAreaComponent.removeRearDisplayStatusListener(listener)
+ }
+ }.distinctUntilChanged()
+ }
+
+ override fun rearDisplayMode(
+ activity: Activity,
+ executor: Executor,
+ windowAreaSessionCallback: WindowAreaSessionCallback
+ ) {
+ // If we already have a status value that is not [WindowAreaStatus.AVAILABLE]
+ // we should throw an exception quick to indicate they tried to enable
+ // RearDisplay mode when it was not available.
+ if (currentStatus != null && currentStatus != WindowAreaStatus.AVAILABLE) {
+ throw WindowAreaController.REAR_DISPLAY_ERROR
+ }
+ rearDisplaySessionConsumer =
+ RearDisplaySessionConsumer(windowAreaSessionCallback, windowAreaComponent)
+ windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
+ }
+
+ internal class RearDisplaySessionConsumer(
+ private val appCallback: WindowAreaSessionCallback,
+ private val extensionsComponent: WindowAreaComponent
+ ) : Consumer<Int> {
+
+ private var session: WindowAreaSession? = null
+
+ override fun accept(sessionStatus: Int) {
+ when (sessionStatus) {
+ WindowAreaComponent.SESSION_STATE_ACTIVE -> onSessionStarted()
+ WindowAreaComponent.SESSION_STATE_INACTIVE -> onSessionFinished()
+ else -> {
+ if (BuildConfig.verificationMode ==
+ SpecificationComputer.VerificationMode.STRICT) {
+ Log.d(TAG, "Received an unknown session status value: $sessionStatus")
+ }
+ onSessionFinished()
+ }
+ }
+ }
+
+ private fun onSessionStarted() {
+ session = RearDisplaySessionImpl(extensionsComponent)
+ session?.let { appCallback.onSessionStarted(it) }
+ }
+
+ private fun onSessionFinished() {
+ session = null
+ appCallback.onSessionEnded()
+ }
+ }
+
+ internal companion object {
+ private val TAG = WindowAreaControllerImpl::class.simpleName
+ /*
+ Chosen as 10 for a base default value. We shouldn't be receiving
+ many changes to window area status so this is enough capacity
+ to not end up blocking.
+ */
+ private const val BUFFER_CAPACITY = 10
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
new file mode 100644
index 0000000..6cdbd12
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+
+/**
+ * Session interface to represent a long-standing
+ * WindowArea mode or feature that provides a handle
+ * to close the session.
+ */
+@ExperimentalWindowApi
+public interface WindowAreaSession {
+ fun close()
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
new file mode 100644
index 0000000..80842c4
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+
+/** Callback to update the client on the WindowArea Session being
+ * started and ended.
+ * TODO(b/207720511) Move to window-java module when Kotlin API Finalized
+ */
+@ExperimentalWindowApi
+interface WindowAreaSessionCallback {
+
+ fun onSessionStarted(session: WindowAreaSession)
+
+ fun onSessionEnded()
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
new file mode 100644
index 0000000..f60d8f5
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+/**
+ * Represents a window area status.
+ */
+@ExperimentalWindowApi
+class WindowAreaStatus private constructor(private val mDescription: String) {
+ override fun toString(): String {
+ return mDescription
+ }
+
+ companion object {
+ /**
+ * Status representing that the WindowArea feature is not a supported
+ * feature on the device.
+ */
+ @JvmField
+ val UNSUPPORTED = WindowAreaStatus("UNSUPPORTED")
+
+ /**
+ * Status representing that the WindowArea feature is currently not available
+ * to be enabled. This could be due to another process has enabled it, or that the
+ * current device configuration doesn't allow it.
+ */
+ @JvmField
+ val UNAVAILABLE = WindowAreaStatus("UNAVAILABLE")
+
+ /**
+ * Status representing that the WindowArea feature is available to be enabled.
+ */
+ @JvmField
+ val AVAILABLE = WindowAreaStatus("AVAILABLE")
+
+ @JvmStatic
+ internal fun translate(status: Int): WindowAreaStatus {
+ return when (status) {
+ WindowAreaComponent.STATUS_AVAILABLE -> AVAILABLE
+ WindowAreaComponent.STATUS_UNAVAILABLE -> UNAVAILABLE
+ else -> UNSUPPORTED
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
new file mode 100644
index 0000000..04d3702
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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 androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import org.junit.Test
+
+/**
+ * Unit tests for [WindowAreaStatus] that run on the JVM.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class WindowAreaStatusUnitTest {
+
+ @Test
+ fun testWindowAreaStatusTranslateValueAvailable() {
+ val expected = WindowAreaStatus.AVAILABLE
+ val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_AVAILABLE)
+ assert(expected == translateValue)
+ }
+
+ @Test
+ fun testWindowAreaStatusTranslateValueUnavailable() {
+ val expected = WindowAreaStatus.UNAVAILABLE
+ val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNAVAILABLE)
+ assert(expected == translateValue)
+ }
+
+ @Test
+ fun testWindowAreaStatusTranslateValueUnsupported() {
+ val expected = WindowAreaStatus.UNSUPPORTED
+ val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNSUPPORTED)
+ assert(expected == translateValue)
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime-ktx/api/2.7.0-beta01.txt b/work/work-runtime-ktx/api/2.7.0-beta01.txt
index 8535d65..2c5f419 100644
--- a/work/work-runtime-ktx/api/2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/2.7.0-beta01.txt
@@ -5,8 +5,6 @@
ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
- method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
- method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
method public final void onStopped();
method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
diff --git a/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt b/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
index 8535d65..2d666e3 100644
--- a/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
@@ -5,8 +5,8 @@
ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
- method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
- method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+ method @androidx.work.ExperimentalExpeditedWork public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
+ method @androidx.work.ExperimentalExpeditedWork public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
method public final void onStopped();
method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
diff --git a/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt b/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
index 8535d65..2c5f419 100644
--- a/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
@@ -5,8 +5,6 @@
ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
- method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
- method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
method public final void onStopped();
method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
diff --git a/work/work-runtime/api/2.7.0-beta01.txt b/work/work-runtime/api/2.7.0-beta01.txt
index a6de624..54713f5 100644
--- a/work/work-runtime/api/2.7.0-beta01.txt
+++ b/work/work-runtime/api/2.7.0-beta01.txt
@@ -154,7 +154,6 @@
public abstract class ListenableWorker {
ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
method public final android.content.Context getApplicationContext();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
method public final java.util.UUID getId();
method public final androidx.work.Data getInputData();
method @RequiresApi(28) public final android.net.Network? getNetwork();
@@ -216,11 +215,6 @@
public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
}
- public enum OutOfQuotaPolicy {
- enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
- enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
- }
-
public final class OverwritingInputMerger extends androidx.work.InputMerger {
ctor public OverwritingInputMerger();
method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
@@ -347,7 +341,6 @@
method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
method public final B setConstraints(androidx.work.Constraints);
- method public B setExpedited(androidx.work.OutOfQuotaPolicy);
method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
method public final B setInputData(androidx.work.Data);
diff --git a/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt b/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt
index a6de624..0c2f419 100644
--- a/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt
+++ b/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt
@@ -129,6 +129,9 @@
enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
}
+ @experimental.Experimental(level=androidx.annotation.experimental.Experimental.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PACKAGE}) public @interface ExperimentalExpeditedWork {
+ }
+
public final class ForegroundInfo {
ctor public ForegroundInfo(int, android.app.Notification);
ctor public ForegroundInfo(int, android.app.Notification, int);
@@ -154,7 +157,7 @@
public abstract class ListenableWorker {
ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
method public final android.content.Context getApplicationContext();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+ method @androidx.work.ExperimentalExpeditedWork public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
method public final java.util.UUID getId();
method public final androidx.work.Data getInputData();
method @RequiresApi(28) public final android.net.Network? getNetwork();
@@ -216,7 +219,7 @@
public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
}
- public enum OutOfQuotaPolicy {
+ @androidx.work.ExperimentalExpeditedWork public enum OutOfQuotaPolicy {
enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
}
@@ -347,7 +350,7 @@
method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
method public final B setConstraints(androidx.work.Constraints);
- method public B setExpedited(androidx.work.OutOfQuotaPolicy);
+ method @androidx.work.ExperimentalExpeditedWork public B setExpedited(androidx.work.OutOfQuotaPolicy);
method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
method public final B setInputData(androidx.work.Data);
diff --git a/work/work-runtime/api/restricted_2.7.0-beta01.txt b/work/work-runtime/api/restricted_2.7.0-beta01.txt
index a6de624..54713f5 100644
--- a/work/work-runtime/api/restricted_2.7.0-beta01.txt
+++ b/work/work-runtime/api/restricted_2.7.0-beta01.txt
@@ -154,7 +154,6 @@
public abstract class ListenableWorker {
ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
method public final android.content.Context getApplicationContext();
- method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
method public final java.util.UUID getId();
method public final androidx.work.Data getInputData();
method @RequiresApi(28) public final android.net.Network? getNetwork();
@@ -216,11 +215,6 @@
public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
}
- public enum OutOfQuotaPolicy {
- enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
- enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
- }
-
public final class OverwritingInputMerger extends androidx.work.InputMerger {
ctor public OverwritingInputMerger();
method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
@@ -347,7 +341,6 @@
method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
method public final B setConstraints(androidx.work.Constraints);
- method public B setExpedited(androidx.work.OutOfQuotaPolicy);
method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
method public final B setInputData(androidx.work.Data);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index ae4eb5c..b956663 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -253,6 +253,7 @@
});
}
+ @SuppressWarnings("deprecation")
@MainThread
private void handleNotify(@NonNull Intent intent) {
int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);