Merge "Macro Benchmark for Semantics" into androidx-main am: 8c1fc2090d
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/3073541
Change-Id: If29f697772dbcdb9681819dc16edf63bc523f091
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 0d8a233..1d02f4a 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -63,5 +63,6 @@
}
android {
+ compileSdk 35
namespace "androidx.activity.compose"
}
diff --git a/activity/activity-compose/integration-tests/activity-demos/build.gradle b/activity/activity-compose/integration-tests/activity-demos/build.gradle
index bfcfdbe..a541a1c 100644
--- a/activity/activity-compose/integration-tests/activity-demos/build.gradle
+++ b/activity/activity-compose/integration-tests/activity-demos/build.gradle
@@ -44,5 +44,6 @@
}
android {
+ compileSdk 35
namespace "androidx.activity.compose.demos"
}
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 9f5fa5b..6086b6d 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -54,5 +54,6 @@
}
android {
+ compileSdk 35
namespace "androidx.activity.compose.samples"
}
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 6cc0206..3d1ee71 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -66,5 +66,6 @@
}
android {
+ compileSdk 35
namespace "androidx.activity.ktx"
}
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index e5aaa29..cbe8aa5 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -305,8 +305,16 @@
}
public final class PickVisualMediaRequest {
+ method public long getAccentColor();
+ method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
method public int getMaxItems();
method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+ method public boolean isCustomAccentColorApplied();
+ method public boolean isOrderedSelection();
+ property public final long accentColor;
+ property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
+ property public final boolean isCustomAccentColorApplied;
+ property public final boolean isOrderedSelection;
property public final int maxItems;
property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
}
@@ -314,13 +322,18 @@
public static final class PickVisualMediaRequest.Builder {
ctor public PickVisualMediaRequest.Builder();
method public androidx.activity.result.PickVisualMediaRequest build();
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
}
public final class PickVisualMediaRequestKt {
method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+ method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
}
}
@@ -415,6 +428,9 @@
method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
}
@@ -423,6 +439,23 @@
method public boolean isPhotoPickerAvailable(android.content.Context context);
}
+ public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public abstract int getValue();
+ property public abstract int value;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public int getValue();
+ property public int value;
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public int getValue();
+ property public int value;
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
+ }
+
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;
}
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index fdca347..b405719 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -304,8 +304,16 @@
}
public final class PickVisualMediaRequest {
+ method public long getAccentColor();
+ method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
method public int getMaxItems();
method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+ method public boolean isCustomAccentColorApplied();
+ method public boolean isOrderedSelection();
+ property public final long accentColor;
+ property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
+ property public final boolean isCustomAccentColorApplied;
+ property public final boolean isOrderedSelection;
property public final int maxItems;
property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
}
@@ -313,13 +321,18 @@
public static final class PickVisualMediaRequest.Builder {
ctor public PickVisualMediaRequest.Builder();
method public androidx.activity.result.PickVisualMediaRequest build();
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+ method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
}
public final class PickVisualMediaRequestKt {
method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+ method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
+ method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
}
}
@@ -414,6 +427,9 @@
method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
+ field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
}
@@ -422,6 +438,23 @@
method public boolean isPhotoPickerAvailable(android.content.Context context);
}
+ public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public abstract int getValue();
+ property public abstract int value;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public int getValue();
+ property public int value;
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
+ }
+
+ public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+ method public int getValue();
+ property public int value;
+ field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
+ }
+
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;
}
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 1cd39fb..c11bee0 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -15,6 +15,7 @@
}
android {
+ compileSdk 35
namespace "androidx.activity"
}
diff --git a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
index 004ca84..d24f8b4 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
@@ -33,6 +33,7 @@
@RunWith(AndroidJUnit4::class)
class EdgeToEdgeTest {
+ @Suppress("DEPRECATION")
@Test
fun enableAuto() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -73,6 +74,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
fun enableCustom() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -117,6 +119,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
fun enableDark() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -153,6 +156,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
fun enableLight() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
index b51e602..abe6540 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
@@ -109,4 +109,43 @@
assertThat(intent.getIntExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, /* defaultValue= */ 0))
.isEqualTo(/* expected= */ 7)
}
+
+ @Test
+ fun testPickVisualMediaRequest_accentColor() {
+ // test default
+ var request = PickVisualMediaRequest()
+ assertThat(request.isCustomAccentColorApplied).isEqualTo(false)
+
+ // test given accent color in PickVisualMediaRequest
+ request = PickVisualMediaRequest(accentColor = 0xffff0000)
+ assertThat(request.isCustomAccentColorApplied).isEqualTo(true)
+ assertThat(request.accentColor).isEqualTo(0xffff0000)
+ }
+
+ @Test
+ fun testPickVisualMediaRequest_defaultTab() {
+ // test default
+ var request = PickVisualMediaRequest()
+ assertThat(request.defaultTab)
+ .isEqualTo(ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab)
+
+ // test given default tab in PickVisualMediaRequest
+ request =
+ PickVisualMediaRequest(
+ defaultTab = ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab
+ )
+ assertThat(request.defaultTab)
+ .isEqualTo(ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab)
+ }
+
+ @Test
+ fun testPickVisualMediaRequest_isOrderedSelection() {
+ // test default
+ var request = PickVisualMediaRequest()
+ assertThat(request.isOrderedSelection).isEqualTo(false)
+
+ // test given isOrderedSelection in PickVisualMediaRequest
+ request = PickVisualMediaRequest(isOrderedSelection = true)
+ assertThat(request.isOrderedSelection).isEqualTo(true)
+ }
}
diff --git a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
index 4f78044..73953fb 100644
--- a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
+++ b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
@@ -254,6 +254,7 @@
@RequiresApi(23)
private class EdgeToEdgeApi23 : EdgeToEdgeBase() {
+ @Suppress("DEPRECATION")
@DoNotInline
override fun setUp(
statusBarStyle: SystemBarStyle,
@@ -273,6 +274,7 @@
@RequiresApi(26)
private open class EdgeToEdgeApi26 : EdgeToEdgeBase() {
+ @Suppress("DEPRECATION")
@DoNotInline
override fun setUp(
statusBarStyle: SystemBarStyle,
@@ -305,6 +307,7 @@
@RequiresApi(29)
private open class EdgeToEdgeApi29 : EdgeToEdgeApi28() {
+ @Suppress("DEPRECATION")
@DoNotInline
override fun setUp(
statusBarStyle: SystemBarStyle,
diff --git a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
index fa023fe..16569cd 100644
--- a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
@@ -17,6 +17,7 @@
package androidx.activity.result
import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
import androidx.annotation.IntRange
@@ -45,6 +46,10 @@
* @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
* @return a PickVisualMediaRequest that contains the given input
*/
+@Deprecated(
+ "Superseded by PickVisualMediaRequest that take optional isOrderedSelection and defaultTab",
+ level = DeprecationLevel.HIDDEN
+) // Binary API compatibility.
@Suppress("MissingJvmstatic")
fun PickVisualMediaRequest(
mediaType: VisualMediaType = ImageAndVideo,
@@ -52,6 +57,61 @@
) = PickVisualMediaRequest.Builder().setMediaType(mediaType).setMaxItems(maxItems).build()
/**
+ * 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
+ * @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
+ * @param isOrderedSelection whether the user can control the order of selected media when using
+ * [PickMultipleVisualMedia] (defaults to false)
+ * @param defaultTab the tab to initially open in the picker (defaults to [DefaultTab.PhotosTab])
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+@Suppress("MissingJvmstatic")
+fun PickVisualMediaRequest(
+ mediaType: VisualMediaType = ImageAndVideo,
+ @IntRange(from = 2) maxItems: Int = PickMultipleVisualMedia.getMaxItems(),
+ isOrderedSelection: Boolean = false,
+ defaultTab: DefaultTab = DefaultTab.PhotosTab
+) =
+ PickVisualMediaRequest.Builder()
+ .setMediaType(mediaType)
+ .setMaxItems(maxItems)
+ .setOrderedSelection(isOrderedSelection)
+ .setDefaultTab(defaultTab)
+ .build()
+
+/**
+ * Creates a request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ *
+ * @param accentColor color long to customize picker accent color
+ * @param mediaType type to go into the PickVisualMediaRequest
+ * @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
+ * @param isOrderedSelection whether the user can control the order of selected media when using
+ * [PickMultipleVisualMedia] (defaults to false)
+ * @param defaultTab the tab to initially open in the picker (defaults to [DefaultTab.PhotosTab])
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+@Suppress("MissingJvmstatic")
+fun PickVisualMediaRequest(
+ accentColor: Long,
+ mediaType: VisualMediaType = ImageAndVideo,
+ @IntRange(from = 2) maxItems: Int = PickMultipleVisualMedia.getMaxItems(),
+ isOrderedSelection: Boolean = false,
+ defaultTab: DefaultTab = DefaultTab.PhotosTab
+) =
+ PickVisualMediaRequest.Builder()
+ .setMediaType(mediaType)
+ .setMaxItems(maxItems)
+ .setOrderedSelection(isOrderedSelection)
+ .setDefaultTab(defaultTab)
+ .setAccentColor(accentColor)
+ .build()
+
+/**
* A request for a
* [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
* [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
@@ -64,11 +124,27 @@
var maxItems: Int = PickMultipleVisualMedia.getMaxItems()
internal set
+ var isOrderedSelection: Boolean = false
+ internal set
+
+ var defaultTab: DefaultTab = DefaultTab.PhotosTab
+ internal set
+
+ var isCustomAccentColorApplied: Boolean = false
+ internal set
+
+ var accentColor: Long = 0
+ internal set
+
/** A builder for constructing [PickVisualMediaRequest] instances. */
class Builder {
private var mediaType: VisualMediaType = ImageAndVideo
private var maxItems: Int = PickMultipleVisualMedia.getMaxItems()
+ private var isOrderedSelection: Boolean = false
+ private var defaultTab: DefaultTab = DefaultTab.PhotosTab
+ private var isCustomAccentColorApplied: Boolean = false
+ private var accentColor: Long = 0
/**
* Set the media type for the [PickVisualMediaRequest].
@@ -97,6 +173,50 @@
}
/**
+ * Set the ordered selection for the [PickVisualMediaRequest].
+ *
+ * Allow the user to control the order in which images are returned to the calling app. This
+ * parameter might be not supported by the underlying photo picker implementation.
+ *
+ * @param isOrderedSelection boolean to enable customisable selection order in the picker
+ * @return This builder.
+ */
+ fun setOrderedSelection(isOrderedSelection: Boolean): Builder {
+ this.isOrderedSelection = isOrderedSelection
+ return this
+ }
+
+ /**
+ * Set the default tab for the [PickVisualMediaRequest].
+ *
+ * The default tab is used to open the preferred view inside the photo picker at first such
+ * as, e.g. [DefaultTab.PhotosTab], [DefaultTab.AlbumsTab]. This parameter might be not
+ * supported by the underlying photo picker implementation.
+ *
+ * @param defaultTab the tab to launch the picker in
+ * @return This builder.
+ */
+ fun setDefaultTab(defaultTab: DefaultTab): Builder {
+ this.defaultTab = defaultTab
+ return this
+ }
+
+ /**
+ * Set the accent color for the [PickVisualMediaRequest].
+ *
+ * The accent color is used to change the main color in the photo picker. This parameter
+ * might be not supported by the underlying photo picker implementation.
+ *
+ * @param accentColor color long to apply as accent to the main color in the picker
+ * @return This builder.
+ */
+ fun setAccentColor(accentColor: Long): Builder {
+ this.accentColor = accentColor
+ this.isCustomAccentColorApplied = true
+ return this
+ }
+
+ /**
* Build the PickVisualMediaRequest specified by this builder.
*
* @return the newly constructed PickVisualMediaRequest.
@@ -105,6 +225,10 @@
PickVisualMediaRequest().apply {
this.mediaType = [email protected]
this.maxItems = [email protected]
+ this.isOrderedSelection = [email protected]
+ this.defaultTab = [email protected]
+ this.isCustomAccentColorApplied = [email protected]
+ this.accentColor = [email protected]
}
}
}
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 ddc4981..45f8b69 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
@@ -33,6 +33,9 @@
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.Companion.getClipDataUris
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.ACTION_SYSTEM_FALLBACK_PICK_IMAGES
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.GMS_ACTION_PICK_IMAGES
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.GMS_EXTRA_PICK_IMAGES_MAX
@@ -629,6 +632,11 @@
const val ACTION_SYSTEM_FALLBACK_PICK_IMAGES =
"androidx.activity.result.contract.action.PICK_IMAGES"
+ internal const val GMS_ACTION_PICK_IMAGES =
+ "com.google.android.gms.provider.action.PICK_IMAGES"
+ internal const val GMS_EXTRA_PICK_IMAGES_MAX =
+ "com.google.android.gms.provider.extra.PICK_IMAGES_MAX"
+
/**
* Extra that will be sent by [PickMultipleVisualMedia] to an Activity that handles
* [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates that maximum number of photos the
@@ -643,10 +651,39 @@
const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX =
"androidx.activity.result.contract.extra.PICK_IMAGES_MAX"
- internal const val GMS_ACTION_PICK_IMAGES =
- "com.google.android.gms.provider.action.PICK_IMAGES"
- internal const val GMS_EXTRA_PICK_IMAGES_MAX =
- "com.google.android.gms.provider.extra.PICK_IMAGES_MAX"
+ /**
+ * Extra that will be sent by [PickVisualMedia] and [PickMultipleVisualMedia] to an
+ * Activity that handles [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates the
+ * preferred default tab of the picker.
+ *
+ * If this extra is not present, the default tab of the picker will be used.
+ */
+ @Suppress("ActionValue")
+ /* Don't include SYSTEM_FALLBACK in the extra */
+ const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB =
+ "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB"
+
+ /**
+ * Extra that will be sent by [PickMultipleVisualMedia] to an Activity that handles
+ * [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates allowing the user to control the
+ * order in which images are returned to the calling app.
+ */
+ @Suppress("ActionValue")
+ /* Don't include SYSTEM_FALLBACK in the extra */
+ const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER =
+ "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER"
+
+ /**
+ * Extra that will be sent by [PickVisualMedia] and [PickMultipleVisualMedia] to an
+ * Activity that handles [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates the
+ * preferred accent color of the picker.
+ *
+ * If this extra is not present, the default accent color of the picker will be used.
+ */
+ @Suppress("ActionValue")
+ /* Don't include SYSTEM_FALLBACK in the extra */
+ const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR =
+ "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR"
/**
* Check if the current device has support for the photo picker by checking the running
@@ -738,18 +775,46 @@
*/
class SingleMimeType(val mimeType: String) : VisualMediaType
+ /** Represents filter input type accepted by the photo picker. */
+ abstract class DefaultTab private constructor() {
+ abstract val value: Int
+
+ /**
+ * [DefaultTab] object used to open the picker in Photos tab (also the default if no
+ * value is provided).
+ */
+ object PhotosTab : DefaultTab() {
+ override val value = MediaStore.PICK_IMAGES_TAB_IMAGES
+ }
+
+ /** [DefaultTab] object used to open the picker in Albums tab. */
+ object AlbumsTab : DefaultTab() {
+ override val value = MediaStore.PICK_IMAGES_TAB_ALBUMS
+ }
+ }
+
@CallSuper
override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
// Check if Photo Picker is available on the device
return if (isSystemPickerAvailable()) {
Intent(MediaStore.ACTION_PICK_IMAGES).apply {
type = getVisualMimeType(input.mediaType)
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+
+ if (input.isCustomAccentColorApplied) {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+ }
}
} else if (isSystemFallbackPickerAvailable(context)) {
val fallbackPicker = checkNotNull(getSystemFallbackPicker(context)).activityInfo
Intent(ACTION_SYSTEM_FALLBACK_PICK_IMAGES).apply {
setClassName(fallbackPicker.applicationInfo.packageName, fallbackPicker.name)
type = getVisualMimeType(input.mediaType)
+ putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+
+ if (input.isCustomAccentColorApplied) {
+ putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+ }
}
} else if (isGmsPickerAvailable(context)) {
val gmsPicker = checkNotNull(getGmsPicker(context)).activityInfo
@@ -847,6 +912,12 @@
}
putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, currentMaxItems)
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, input.isOrderedSelection)
+
+ if (input.isCustomAccentColorApplied) {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+ }
}
} else if (PickVisualMedia.isSystemFallbackPickerAvailable(context)) {
val fallbackPicker = checkNotNull(getSystemFallbackPicker(context)).activityInfo
@@ -858,6 +929,12 @@
require(currentMaxItems > 1) { "Max items must be greater than 1" }
putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX, currentMaxItems)
+ putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+ putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER, input.isOrderedSelection)
+
+ if (input.isCustomAccentColorApplied) {
+ putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+ }
}
} else if (PickVisualMedia.isGmsPickerAvailable(context)) {
val gmsPicker = checkNotNull(getGmsPicker(context)).activityInfo
diff --git a/activity/integration-tests/macrobenchmark-target/build.gradle b/activity/integration-tests/macrobenchmark-target/build.gradle
index 68c0b8a..e984599 100644
--- a/activity/integration-tests/macrobenchmark-target/build.gradle
+++ b/activity/integration-tests/macrobenchmark-target/build.gradle
@@ -12,7 +12,8 @@
}
android {
- namespace "androidx.activity.integration.macrobenchmark.target"
+ compileSdk 35
+ namespace "androidx.activity.integration.macrobenchmark.target"
}
dependencies {
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index 09b6006..7fbf614 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdk 35
defaultConfig {
applicationId "androidx.activity.integration.testapp"
}
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 9ab8601..5b0c417 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
@@ -131,19 +131,40 @@
PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif"))
)
}
+ button("Pick an image & show albums tab (w/ photo picker)") {
+ pickVisualMedia.launch(
+ PickVisualMediaRequest(
+ mediaType = PickVisualMedia.ImageOnly,
+ defaultTab = PickVisualMedia.DefaultTab.AlbumsTab
+ )
+ )
+ }
+ button("Pick an image & green accent color (w/ photo picker)") {
+ pickVisualMedia.launch(
+ PickVisualMediaRequest(
+ mediaType = PickVisualMedia.ImageOnly,
+ accentColor = 0xFF123456
+ )
+ )
+ }
button("Pick 5 visual media max (w/ photo picker)") {
pickMultipleVisualMedia.launch(
PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
)
}
- button("Pick 9 visual media max (w/ photo picker)") {
+ button("Pick 3 visual media max (w/ photo picker)") {
pickMultipleVisualMedia.launch(
PickVisualMediaRequest(
mediaType = PickVisualMedia.ImageAndVideo,
- maxItems = 9
+ maxItems = 3
)
)
}
+ button("Pick 5 visual media max (w/ photo picker) & selection order") {
+ pickMultipleVisualMedia.launch(
+ PickVisualMediaRequest(isOrderedSelection = true)
+ )
+ }
button("Create document") { createDocument.launch("Temp") }
button("Open documents") { openDocuments.launch(arrayOf("*/*")) }
button("Start IntentSender") {
diff --git a/appcompat/appcompat-lint/integration-tests/build.gradle b/appcompat/appcompat-lint/integration-tests/build.gradle
index cd7691f..4c2adc6 100644
--- a/appcompat/appcompat-lint/integration-tests/build.gradle
+++ b/appcompat/appcompat-lint/integration-tests/build.gradle
@@ -18,6 +18,7 @@
}
android {
+ compileSdk 35
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
diff --git a/appcompat/integration-tests/receive-content-testapp/build.gradle b/appcompat/integration-tests/receive-content-testapp/build.gradle
index e414862..4c0dcc7 100644
--- a/appcompat/integration-tests/receive-content-testapp/build.gradle
+++ b/appcompat/integration-tests/receive-content-testapp/build.gradle
@@ -20,6 +20,7 @@
}
android {
+ compileSdk 35
namespace "androidx.appcompat.demo.receivecontent"
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
index 9d17a58..d98a917 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
@@ -23,11 +23,13 @@
import android.os.Bundle;
import android.text.TextUtils;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.util.Preconditions;
@@ -61,6 +63,9 @@
+ "Please use androidx.appsearch.app.ShortcutAdapter.DEFAULT_NAMESPACE as the "
+ "namespace of the document if it will be used to create a shortcut.";
+ private static final String APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE =
+ "appsearch_generic_doc_parcel";
+
/**
* Converts given document to a {@link ShortcutInfoCompat.Builder}, which can be used to
* construct a shortcut for donation through
@@ -117,16 +122,19 @@
throw new IllegalArgumentException(NAMESPACE_CHECK_ERROR_MESSAGE);
}
final String name = doc.getPropertyString(FIELD_NAME);
+ final Bundle extras = new Bundle();
+ extras.putParcelable(APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, doc.getDocumentParcel());
return new ShortcutInfoCompat.Builder(context, doc.getId())
.setShortLabel(!TextUtils.isEmpty(name) ? name : doc.getId())
.setIntent(new Intent(Intent.ACTION_VIEW, getDocumentUri(doc)))
.setExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)
- .setTransientExtras(doc.getBundle());
+ .setTransientExtras(extras);
}
/**
* Extracts {@link GenericDocument} from given {@link ShortcutInfoCompat} if applicable.
* Returns null if document cannot be found in the given shortcut.
+ *
* @exportToFramework:hide
*/
@Nullable
@@ -137,7 +145,21 @@
if (extras == null) {
return null;
}
- return new GenericDocument(extras);
+
+ GenericDocumentParcel genericDocParcel;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ genericDocParcel = Api33Impl.getParcelableFromBundle(extras,
+ APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, GenericDocumentParcel.class);
+ } else {
+ @SuppressWarnings("deprecation")
+ GenericDocumentParcel tmp = (GenericDocumentParcel) extras.getParcelable(
+ APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE);
+ genericDocParcel = tmp;
+ }
+ if (genericDocParcel == null) {
+ return null;
+ }
+ return new GenericDocument(genericDocParcel);
}
/**
@@ -177,4 +199,21 @@
.path(DEFAULT_NAMESPACE + "/" + id)
.build();
}
+ @RequiresApi(33)
+ static class Api33Impl {
+ private Api33Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static <T> T getParcelableFromBundle(
+ @NonNull Bundle bundle,
+ @NonNull String key,
+ @NonNull Class<T> clazz) {
+ Preconditions.checkNotNull(bundle);
+ Preconditions.checkNotNull(key);
+ Preconditions.checkNotNull(clazz);
+ return bundle.getParcelable(key, clazz);
+ }
+ }
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
index 2c40844..f84153b 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
@@ -73,6 +73,6 @@
@Override
public int hashCode() {
- return Objects.hash(mAsText);
+ return Objects.hashCode(mAsText);
}
}
diff --git a/appsearch/appsearch-debug-view/build.gradle b/appsearch/appsearch-debug-view/build.gradle
index 5d774b7..5472f31 100644
--- a/appsearch/appsearch-debug-view/build.gradle
+++ b/appsearch/appsearch-debug-view/build.gradle
@@ -29,6 +29,7 @@
}
android {
+ compileSdk 35
namespace "androidx.appsearch.debugview"
}
diff --git a/appsearch/appsearch-debug-view/samples/build.gradle b/appsearch/appsearch-debug-view/samples/build.gradle
index 25b9618..3321369 100644
--- a/appsearch/appsearch-debug-view/samples/build.gradle
+++ b/appsearch/appsearch-debug-view/samples/build.gradle
@@ -29,6 +29,7 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.appsearch.debugview.samples"
}
diff --git a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
index 5d84149..17f6a11 100644
--- a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
+++ b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
@@ -31,6 +31,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
import androidx.appsearch.debugview.samples.model.Note;
import androidx.appsearch.debugview.view.AppSearchDebugActivity;
import androidx.core.content.ContextCompat;
@@ -49,7 +50,6 @@
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.Executors;
/**
* Default Activity for AppSearch Debug View Sample App
@@ -80,7 +80,8 @@
mListView = findViewById(R.id.list_view);
mLoadingView = findViewById(R.id.text_view);
- mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+ mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+ .getEnvironmentInstance().createCachedThreadPoolExecutor());
mNotesAppSearchManagerFuture.setFuture(NotesAppSearchManager.createNotesAppSearchManager(
getApplicationContext(), mBackgroundExecutor));
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 a6a79da..79a4c6b 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
@@ -165,7 +165,7 @@
SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
.setResultCountPerPage(PAGE_SIZE)
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
- .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList());
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList());
String retrieveAllQueryString = "";
if (mSearchType == AppSearchDebugActivity.SEARCH_TYPE_GLOBAL) {
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 8c979f8..08320075 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
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
import androidx.appsearch.debugview.DebugAppSearchManager;
import androidx.appsearch.debugview.R;
import androidx.appsearch.exceptions.AppSearchException;
@@ -36,7 +37,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.Executors;
/**
* Debug Activity for AppSearch.
@@ -105,7 +105,8 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appsearchdebug);
- mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+ mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+ .getEnvironmentInstance().createCachedThreadPoolExecutor());
mDbName = getIntent().getExtras().getString(DB_INTENT_KEY);
String targetPackageName =
getIntent().getExtras().getString(TARGET_PACKAGE_NAME_INTENT_KEY);
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 3833958..ae89b19 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
@@ -20,6 +20,10 @@
import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_PACKAGE_NAME;
+import static androidx.appsearch.testutil.AppSearchTestUtils.createMockVisibilityChecker;
import static com.google.common.truth.Truth.assertThat;
@@ -27,11 +31,13 @@
import android.content.Context;
+import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SearchResult;
@@ -41,7 +47,6 @@
import androidx.appsearch.app.SearchSuggestionSpec;
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;
@@ -49,6 +54,7 @@
import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityToDocumentConverter;
import androidx.appsearch.observer.DocumentChangeInfo;
import androidx.appsearch.observer.ObserverSpec;
import androidx.appsearch.observer.SchemaChangeInfo;
@@ -58,6 +64,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.FlakyTest;
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
import com.google.android.icing.proto.DebugInfoProto;
import com.google.android.icing.proto.DebugInfoVerbosity;
import com.google.android.icing.proto.DocumentProto;
@@ -72,7 +81,9 @@
import com.google.android.icing.proto.StorageInfoProto;
import com.google.android.icing.proto.StringIndexingConfig;
import com.google.android.icing.proto.TermMatchType;
+import com.google.android.icing.protobuf.ByteString;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
@@ -117,8 +128,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
}
@After
@@ -400,7 +411,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -454,7 +465,7 @@
mContext.getPackageName(),
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -502,7 +513,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()),
- initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+ initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
// Check recovery state
InitializeStats initStats = initStatsBuilder.build();
@@ -539,7 +550,7 @@
mContext.getPackageName(),
"database1",
Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -585,7 +596,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -598,7 +609,7 @@
"package2",
"database2",
schema2,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -650,7 +661,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -663,7 +674,7 @@
"package2",
"database2",
schema2,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -727,8 +738,7 @@
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
// We need to share across packages
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -736,8 +746,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
// Insert package1 schema
List<AppSearchSchema> personSchema =
@@ -746,7 +756,7 @@
"package1",
"database1",
personSchema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -764,7 +774,7 @@
"package2",
"database2",
callSchema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -777,7 +787,7 @@
"package3",
"database3",
textSchema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -903,8 +913,7 @@
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
// We need to share across packages
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -912,8 +921,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
AppSearchSchema.StringPropertyConfig personField =
new AppSearchSchema.StringPropertyConfig.Builder("personId")
@@ -931,7 +940,7 @@
"package1",
"database1",
personAndCallSchema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -945,7 +954,7 @@
"package2",
"database2",
callSchema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1107,7 +1116,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1166,7 +1175,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1215,7 +1224,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1273,7 +1282,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1334,7 +1343,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1392,7 +1401,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1423,7 +1432,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1476,7 +1485,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1539,7 +1548,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1595,7 +1604,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1661,7 +1670,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1721,7 +1730,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1765,7 +1774,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1828,7 +1837,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1891,7 +1900,7 @@
"package1",
"database1",
schema1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1979,7 +1988,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -1989,7 +1998,9 @@
SchemaProto expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database1/Email").setVersion(0))
+ .setSchemaType("package$database1/Email")
+ .setDescription("")
+ .setVersion(0))
.build();
List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
@@ -2016,7 +2027,7 @@
"package",
"database1",
oldSchemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2031,7 +2042,7 @@
"package",
"database1",
newSchemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2055,7 +2066,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2065,9 +2076,14 @@
SchemaProto expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database1/Email").setVersion(0))
- .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
- "package$database1/Document").setVersion(0))
+ .setSchemaType("package$database1/Email")
+ .setDescription("")
+ .setVersion(0))
+ .addTypes(
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database1/Document")
+ .setDescription("")
+ .setVersion(0))
.build();
// Check both schema Email and Document saved correctly.
@@ -2083,7 +2099,7 @@
"package",
"database1",
finalSchemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2098,7 +2114,7 @@
"package",
"database1",
finalSchemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2108,7 +2124,9 @@
expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database1/Email").setVersion(0))
+ .setSchemaType("package$database1/Email")
+ .setDescription("")
+ .setVersion(0))
.build();
expectedTypes = new ArrayList<>();
@@ -2133,7 +2151,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2142,7 +2160,7 @@
"package",
"database2",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2152,14 +2170,24 @@
SchemaProto expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database1/Email").setVersion(0))
- .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
- "package$database1/Document").setVersion(0))
+ .setSchemaType("package$database1/Email")
+ .setDescription("")
+ .setVersion(0))
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database2/Email").setVersion(0))
- .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
- "package$database2/Document").setVersion(0))
+ .setSchemaType("package$database1/Document")
+ .setDescription("")
+ .setVersion(0))
+ .addTypes(
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database2/Email")
+ .setDescription("")
+ .setVersion(0))
+ .addTypes(
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database2/Document")
+ .setDescription("")
+ .setVersion(0))
.build();
// Check Email and Document is saved in database 1 and 2 correctly.
@@ -2175,7 +2203,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2186,12 +2214,19 @@
expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database1/Email").setVersion(0))
+ .setSchemaType("package$database1/Email")
+ .setDescription("")
+ .setVersion(0))
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("package$database2/Email").setVersion(0))
- .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
- "package$database2/Document").setVersion(0))
+ .setSchemaType("package$database2/Email")
+ .setDescription("")
+ .setVersion(0))
+ .addTypes(
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database2/Document")
+ .setDescription("")
+ .setVersion(0))
.build();
// Check nothing changed in database2.
@@ -2215,7 +2250,7 @@
"package",
"database",
schema,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2265,8 +2300,8 @@
existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
}
- // Create VisibilityDocument
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("schema")
+ // Create VisibilityConfig
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -2278,7 +2313,7 @@
"packageA",
"database",
schema,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2287,7 +2322,7 @@
"packageB",
"database",
schema,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2297,10 +2332,14 @@
SchemaProto expectedProto = SchemaProto.newBuilder()
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("packageA$database/schema").setVersion(0))
+ .setSchemaType("packageA$database/schema")
+ .setDescription("")
+ .setVersion(0))
.addTypes(
SchemaTypeConfigProto.newBuilder()
- .setSchemaType("packageB$database/schema").setVersion(0))
+ .setSchemaType("packageB$database/schema")
+ .setDescription("")
+ .setVersion(0))
.build();
List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
expectedTypes.addAll(existingSchemas);
@@ -2309,22 +2348,22 @@
.containsExactlyElementsIn(expectedTypes);
// Verify these two visibility documents are stored in AppSearch.
- VisibilityDocument expectedVisibilityDocumentA =
- new VisibilityDocument.Builder("packageA$database/schema")
+ InternalVisibilityConfig expectedVisibilityConfigA =
+ new InternalVisibilityConfig.Builder("packageA$database/schema")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
- VisibilityDocument expectedVisibilityDocumentB =
- new VisibilityDocument.Builder("packageB$database/schema")
+ InternalVisibilityConfig expectedVisibilityConfigB =
+ new InternalVisibilityConfig.Builder("packageB$database/schema")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
assertThat(mAppSearchImpl.mVisibilityStoreLocked
.getVisibility("packageA$database/schema"))
- .isEqualTo(expectedVisibilityDocumentA);
+ .isEqualTo(expectedVisibilityConfigA);
assertThat(mAppSearchImpl.mVisibilityStoreLocked
.getVisibility("packageB$database/schema"))
- .isEqualTo(expectedVisibilityDocumentB);
+ .isEqualTo(expectedVisibilityConfigB);
// Prune packages
mAppSearchImpl.prunePackageData(existingPackages);
@@ -2353,7 +2392,7 @@
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package1", "database1",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2366,7 +2405,7 @@
internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package1", "database2",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2379,7 +2418,7 @@
internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package2", "database1",
Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2401,7 +2440,7 @@
"package1",
"database1",
schemas1,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2410,7 +2449,7 @@
"package1",
"database2",
schemas2,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2419,7 +2458,7 @@
"package2",
"database1",
schemas3,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2429,7 +2468,8 @@
"package1$database2/type2",
"package2$database1/type3",
"VS#Pkg$VS#Db/VisibilityType", // plus the stored Visibility schema
- "VS#Pkg$VS#Db/VisibilityPermissionType");
+ "VS#Pkg$VS#Db/VisibilityPermissionType",
+ "VS#Pkg$VS#AndroidVDb/AndroidVOverlayType");
}
@FlakyTest(bugId = 204186664)
@@ -2442,7 +2482,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2541,7 +2581,7 @@
"package1",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2564,7 +2604,7 @@
"package1",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2585,7 +2625,7 @@
"package2",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2643,7 +2683,7 @@
"package1",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2666,7 +2706,7 @@
"package1",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2688,7 +2728,7 @@
"package1",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2697,7 +2737,7 @@
"package1",
"database2",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2755,7 +2795,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2768,7 +2808,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null));
@@ -2840,7 +2880,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2870,8 +2910,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
"id1",
Collections.emptyMap());
@@ -2887,7 +2927,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -2942,8 +2982,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
"database",
"namespace1",
@@ -2964,7 +3004,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3021,8 +3061,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
"database",
"namespace1",
@@ -3043,7 +3083,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3077,7 +3117,7 @@
.isEqualTo(2);
assertThat(
storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
- .isEqualTo(3); // +2 for VisibilitySchema
+ .isEqualTo(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
}
@Test
@@ -3088,7 +3128,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3122,7 +3162,7 @@
debugInfo.getDocumentInfo().getDocumentStorageInfo().getNumAliveDocuments())
.isEqualTo(2);
assertThat(debugInfo.getSchemaInfo().getSchema().getTypesList())
- .hasSize(3); // +2 for VisibilitySchema
+ .hasSize(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
}
@Test
@@ -3146,8 +3186,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas =
@@ -3156,7 +3196,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3225,8 +3265,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas =
@@ -3235,7 +3275,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3282,8 +3322,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Make sure the limit is maintained
e = assertThrows(AppSearchException.class, () ->
@@ -3319,8 +3359,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas =
@@ -3329,7 +3369,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3432,8 +3472,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas =
@@ -3442,7 +3482,7 @@
"package1",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3451,7 +3491,7 @@
"package1",
"database2",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3460,7 +3500,7 @@
"package2",
"database1",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3469,7 +3509,7 @@
"package2",
"database2",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3528,8 +3568,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// package1 should still be out of space
e = assertThrows(AppSearchException.class, () ->
@@ -3585,8 +3625,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3602,7 +3642,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3738,8 +3778,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3751,7 +3791,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3821,8 +3861,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Insert schema
List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3834,7 +3874,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -3878,8 +3918,8 @@
return Integer.MAX_VALUE;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
// Index id2. This should pass but only because we check for replacements.
mAppSearchImpl.putDocument(
@@ -3924,8 +3964,8 @@
return 2;
}
}, new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
AppSearchException e = assertThrows(AppSearchException.class, () ->
mAppSearchImpl.searchSuggestion(
@@ -3950,7 +3990,7 @@
mContext.getPackageName(),
"database1",
/*schemas=*/ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/false,
/*version=*/0,
/*setSchemaStatsBuilder=*/null);
@@ -4020,8 +4060,7 @@
// Create a new mAppSearchImpl with a mock Visibility Checker
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(false);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4029,14 +4068,14 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4073,8 +4112,7 @@
// Create a new mAppSearchImpl with a mock Visibility Checker
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4082,14 +4120,14 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4124,8 +4162,7 @@
// Create a new mAppSearchImpl with a mock Visibility Checker
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4133,14 +4170,14 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4176,9 +4213,20 @@
// 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");
+ VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return callerAccess.getCallingPackageName().equals("visiblePackage");
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
+
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4186,14 +4234,14 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4237,7 +4285,7 @@
@Test
public void testSetVisibility() throws Exception {
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -4249,7 +4297,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4257,27 +4305,31 @@
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]))
- .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ InternalVisibilityConfig expectedDocument =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix + "Email"))
.isEqualTo(expectedDocument);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix + "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualDocument =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument).isEqualTo(expectedDocument);
}
@Test
public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
// Create Visibility Document for Email1
- VisibilityDocument visibilityDocument1 = new VisibilityDocument.Builder("Email1")
+ InternalVisibilityConfig visibilityConfig1 = new InternalVisibilityConfig.Builder("Email1")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -4289,7 +4341,7 @@
"package1",
"database",
schemas1,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument1),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig1),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4297,24 +4349,29 @@
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]))
- .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+ InternalVisibilityConfig expectedDocument1 =
+ new InternalVisibilityConfig.Builder(prefix1 + "Email1")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix1 + "Email1"))
.isEqualTo(expectedDocument1);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument1 =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix1 + "Email1",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualDocument1 =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix1 + "Email1",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
+
assertThat(actualDocument1).isEqualTo(expectedDocument1);
// Create Visibility Document for Email2
- VisibilityDocument visibilityDocument2 = new VisibilityDocument.Builder("Email2")
+ InternalVisibilityConfig visibilityConfig2 = new InternalVisibilityConfig.Builder("Email2")
.setNotDisplayedBySystem(false)
.addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
.build();
@@ -4326,7 +4383,7 @@
"package2",
"database",
schemas2,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument2),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig2),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4334,39 +4391,46 @@
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]))
- .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
+ InternalVisibilityConfig expectedDocument2 =
+ new InternalVisibilityConfig.Builder(prefix2 + "Email2")
+ .setNotDisplayedBySystem(false)
+ .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix2 + "Email2"))
.isEqualTo(expectedDocument2);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument2 = new VisibilityDocument.Builder(
- mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix2 + "Email2",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualDocument2 =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix2 + "Email2",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument2).isEqualTo(expectedDocument2);
// Check the existing visibility document retains.
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix1 + "Email1"))
.isEqualTo(expectedDocument1);
// Verify the VisibilityDocument is saved to AppSearchImpl.
- actualDocument1 = new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix1 + "Email1",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ actualDocument1 = VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix1 + "Email1",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument1).isEqualTo(expectedDocument1);
}
@Test
public void testSetVisibility_removeVisibilitySettings() throws Exception {
// Create a non-all-default visibility document
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -4379,26 +4443,29 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
String prefix = PrefixUtil.createPrefix("package", "database1");
- VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
- .setNotDisplayedBySystem(true)
- .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
- .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ InternalVisibilityConfig expectedDocument =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix + "Email"))
.isEqualTo(expectedDocument);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix + "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ InternalVisibilityConfig actualDocument =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument).isEqualTo(expectedDocument);
// Set schema Email and its all-default visibility document to AppSearch database1
@@ -4406,7 +4473,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(),
+ /*visibilityConfigs=*/ ImmutableList.of(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4414,12 +4481,12 @@
// All-default visibility document won't be saved in AppSearch.
assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
.isNull();
- // Verify the VisibilityDocument is removed from AppSearchImpl.
+ // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
AppSearchException e = assertThrows(AppSearchException.class,
() -> mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Email",
/*typePropertyPaths=*/ Collections.emptyMap()));
assertThat(e).hasMessageThat().contains(
@@ -4429,7 +4496,7 @@
@Test
public void testRemoveVisibility_noRemainingSettings() throws Exception {
// Create a non-all-default visibility document
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -4442,25 +4509,29 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+ /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
/*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]))
- .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ InternalVisibilityConfig expectedDocument =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix + "Email"))
.isEqualTo(expectedDocument);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix + "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualDocument =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument).isEqualTo(expectedDocument);
// remove the schema and visibility setting from AppSearch
@@ -4468,7 +4539,7 @@
"package",
"database1",
/*schemas=*/ new ArrayList<>(),
- /*visibilityDocuments=*/ ImmutableList.of(),
+ /*visibilityConfigs=*/ ImmutableList.of(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4478,7 +4549,7 @@
"package",
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(),
+ /*visibilityConfigs=*/ ImmutableList.of(),
/*forceOverride=*/ false,
/*version=*/ 0,
/* setSchemaStatsBuilder= */ null);
@@ -4488,9 +4559,9 @@
// Verify there is no visibility setting for the schema.
AppSearchException e = assertThrows(AppSearchException.class,
() -> mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Email",
/*typePropertyPaths=*/ Collections.emptyMap()));
assertThat(e).hasMessageThat().contains(
@@ -4500,7 +4571,7 @@
@Test
public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
// set Schema and visibility to AppSearch
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
@@ -4510,7 +4581,7 @@
"packageName",
"databaseName",
schemas,
- ImmutableList.of(visibilityDocument),
+ ImmutableList.of(visibilityConfig),
/*forceOverride=*/ true,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -4525,25 +4596,29 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
- VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
- .setNotDisplayedBySystem(true)
- .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
- .build();
+ InternalVisibilityConfig expectedDocument =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
- assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+ assertThat(mAppSearchImpl.mVisibilityStoreLocked
+ .getVisibility(prefix + "Email"))
.isEqualTo(expectedDocument);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix + "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualDocument =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
assertThat(actualDocument).isEqualTo(expectedDocument);
// remove schema and visibility document
@@ -4566,16 +4641,16 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
- // Verify the VisibilityDocument is removed from AppSearchImpl.
+ // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
AppSearchException e = assertThrows(AppSearchException.class,
() -> mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Email",
/*typePropertyPaths=*/ Collections.emptyMap()));
assertThat(e).hasMessageThat().contains(
@@ -4590,8 +4665,7 @@
// Create a new mAppSearchImpl with a mock Visibility Checker
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+ VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4599,16 +4673,16 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema type that is not displayed by the system
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ImmutableList.of(
- new VisibilityDocument.Builder("Type")
+ /*visibilityConfigs=*/ImmutableList.of(
+ new InternalVisibilityConfig.Builder("Type")
.setNotDisplayedBySystem(true).build()),
/*forceOverride=*/false,
/*version=*/0,
@@ -4631,7 +4705,7 @@
"package",
"database",
Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
- /*visibilityDocuments=*/ImmutableList.of(),
+ /*visibilityConfigs=*/ImmutableList.of(),
/*forceOverride=*/false,
/*version=*/0,
/*setSchemaStatsBuilder=*/null);
@@ -4655,7 +4729,7 @@
"package",
"database",
schemas,
- /*visibilityDocuments=*/ImmutableList.of(),
+ /*visibilityConfigs=*/ImmutableList.of(),
/*forceOverride=*/false,
/*version=*/1,
/*setSchemaStatsBuilder=*/null);
@@ -4687,9 +4761,20 @@
// Create a new mAppSearchImpl with a mock Visibility Checker
mAppSearchImpl.close();
File tempFolder = mTemporaryFolder.newFolder();
- VisibilityChecker mockVisibilityChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore)
- -> prefixedSchema.endsWith("VisibleType");
+ VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return prefixedSchema.endsWith("VisibleType");
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
+
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new AppSearchConfigImpl(
@@ -4697,19 +4782,19 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- mockVisibilityChecker);
+ mockVisibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add two schema types that are not displayed by the system.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
"package",
"database",
schemas,
- /*visibilityDocuments=*/ImmutableList.of(
- new VisibilityDocument.Builder("VisibleType")
+ /*visibilityConfigs=*/ImmutableList.of(
+ new InternalVisibilityConfig.Builder("VisibleType")
.setNotDisplayedBySystem(true)
.build(),
- new VisibilityDocument.Builder("PrivateType")
+ new InternalVisibilityConfig.Builder("PrivateType")
.setNotDisplayedBySystem(true)
.build()),
/*forceOverride=*/false,
@@ -4728,13 +4813,258 @@
}
@Test
+ public void testGetSchema_global_publicAcl() throws Exception {
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("PublicTypeA").build(),
+ new AppSearchSchema.Builder("PublicTypeB").build(),
+ new AppSearchSchema.Builder("PublicTypeC").build());
+
+ PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+ PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+ PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+
+ // Package A is visible to package B & C, package B is visible to package C (based on
+ // canPackageQuery, which we are mocking).
+ Map<String, Set<String>> packageCanSee = ImmutableMap.of(
+ "A", ImmutableSet.of("A"),
+ "B", ImmutableSet.of("A", "B"),
+ "C", ImmutableSet.of("A", "B", "C"));
+ final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+ return packageCanSee.get(callerAccess.getCallingPackageName())
+ .contains(param.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+ .getPackageName());
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
+
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new AppSearchConfigImpl(
+ new UnlimitedLimitConfig(),
+ new LocalStorageIcingOptionsConfig()
+ ),
+ /*initStatsBuilder=*/ null,
+ publicAclMockChecker, ALWAYS_OPTIMIZE
+ );
+
+ List<InternalVisibilityConfig> visibilityConfigs = ImmutableList.of(
+ new InternalVisibilityConfig.Builder("PublicTypeA")
+ .setPubliclyVisibleTargetPackage(pkgA).build(),
+ new InternalVisibilityConfig.Builder("PublicTypeB")
+ .setPubliclyVisibleTargetPackage(pkgB).build(),
+ new InternalVisibilityConfig.Builder("PublicTypeC")
+ .setPubliclyVisibleTargetPackage(pkgC).build());
+
+ // Add the three schema types, each with their own publicly visible target package.
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ visibilityConfigs,
+ /*forceOverride=*/true,
+ /*version=*/1,
+ /*setSchemaStatsBuilder=*/null);
+ assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+ // Verify access to schemas based on calling package
+ GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(pkgA.getPackageName()));
+ assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+
+ getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(pkgB.getPackageName()));
+ assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0), schemas.get(1));
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+
+ getResponse = mAppSearchImpl.getSchema(
+ "package",
+ "database",
+ new CallerAccess(pkgC.getPackageName()));
+ assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+ assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeC");
+ }
+
+ @Test
+ public void testGetSchema_global_publicAcl_removal() throws Exception {
+ // This test to ensure the proper documents are created through setSchema, then removed
+ // when setSchema is called again
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("PublicTypeA").build(),
+ new AppSearchSchema.Builder("PublicTypeB").build(),
+ new AppSearchSchema.Builder("PublicTypeC").build());
+
+ PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+ PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+ PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+ // Create a new mAppSearchImpl with a mock Visibility Checker
+ mAppSearchImpl.close();
+ File tempFolder = mTemporaryFolder.newFolder();
+
+ // Package A is visible to package B & C, package B is visible to package C (based on
+ // canPackageQuery, which we are mocking).
+ Map<String, Set<String>> packageCanSee = ImmutableMap.of(
+ "A", ImmutableSet.of("A"),
+ "B", ImmutableSet.of("A", "B"),
+ "C", ImmutableSet.of("A", "B", "C"));
+ final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+ return packageCanSee.get(callerAccess.getCallingPackageName())
+ .contains(param.getVisibilityConfig()
+ .getPubliclyVisibleTargetPackage().getPackageName());
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
+
+ mAppSearchImpl = AppSearchImpl.create(
+ tempFolder,
+ new AppSearchConfigImpl(
+ new UnlimitedLimitConfig(),
+ new LocalStorageIcingOptionsConfig()
+ ),
+ /*initStatsBuilder=*/ null,
+ publicAclMockChecker, ALWAYS_OPTIMIZE
+ );
+
+ List<InternalVisibilityConfig> visibilityConfigs = ImmutableList.of(
+ new InternalVisibilityConfig.Builder("PublicTypeA")
+ .setPubliclyVisibleTargetPackage(pkgA).build(),
+ new InternalVisibilityConfig.Builder("PublicTypeB")
+ .setPubliclyVisibleTargetPackage(pkgB).build(),
+ new InternalVisibilityConfig.Builder("PublicTypeC")
+ .setPubliclyVisibleTargetPackage(pkgC).build());
+
+ // Add two schema types that are not displayed by the system.
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ visibilityConfigs,
+ /*forceOverride=*/true,
+ /*version=*/1,
+ /*setSchemaStatsBuilder=*/null);
+ assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+ // Now check for documents
+ GenericDocument visibilityOverlayA = mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeA",
+ Collections.emptyMap());
+ GenericDocument visibilityOverlayB = mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeB",
+ Collections.emptyMap());
+ GenericDocument visibilityOverlayC = mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeC",
+ Collections.emptyMap());
+
+ AndroidVOverlayProto overlayProtoA = AndroidVOverlayProto.newBuilder()
+ .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("A")
+ .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+ .build())
+ .build();
+ AndroidVOverlayProto overlayProtoB = AndroidVOverlayProto.newBuilder()
+ .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("B")
+ .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+ .build())
+ .build();
+ AndroidVOverlayProto overlayProtoC = AndroidVOverlayProto.newBuilder()
+ .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("C")
+ .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+ .build())
+ .build();
+
+ assertThat(visibilityOverlayA.getPropertyBytes("visibilityProtoSerializeProperty"))
+ .isEqualTo(overlayProtoA.toByteArray());
+ assertThat(visibilityOverlayB.getPropertyBytes("visibilityProtoSerializeProperty"))
+ .isEqualTo(overlayProtoB.toByteArray());
+ assertThat(visibilityOverlayC.getPropertyBytes("visibilityProtoSerializeProperty"))
+ .isEqualTo(overlayProtoC.toByteArray());
+
+ // now undo the "public" setting
+ visibilityConfigs = ImmutableList.of(
+ new InternalVisibilityConfig.Builder("PublicTypeA").build(),
+ new InternalVisibilityConfig.Builder("PublicTypeB").build(),
+ new InternalVisibilityConfig.Builder("PublicTypeC").build());
+
+ InternalSetSchemaResponse internalSetSchemaResponseRemoved = mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ schemas,
+ visibilityConfigs,
+ /*forceOverride=*/true,
+ /*version=*/1,
+ /*setSchemaStatsBuilder=*/null);
+ assertThat(internalSetSchemaResponseRemoved.isSuccess()).isTrue();
+
+ // Now check for documents again
+ Exception e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeA", Collections.emptyMap()));
+ assertThat(e.getMessage()).endsWith("not found.");
+ e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeB", Collections.emptyMap()));
+ assertThat(e.getMessage()).endsWith("not found.");
+ e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ "package$database/PublicTypeC", Collections.emptyMap()));
+ assertThat(e.getMessage()).endsWith("not found.");
+ }
+
+ @Test
public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
// Add a schema type
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -4776,8 +5106,7 @@
@Test
public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
// Make a visibility checker that rejects everything
- final VisibilityChecker rejectChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -4786,15 +5115,15 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- rejectChecker);
+ rejectChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema type
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -4840,7 +5169,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -4850,7 +5179,7 @@
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.registerObserverCallback(
new CallerAccess(/*callingPackageName=*/
- "com.fake.Listening.package"),
+ "com.fake.Listening.package"),
/*targetPackageName=*/mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -4879,9 +5208,19 @@
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);
+ final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -4890,15 +5229,15 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- visibilityChecker);
+ visibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema type
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -4942,8 +5281,7 @@
final String fakeListeningPackage = "com.fake.Listening.package";
// Make a visibility checker that rejects everything.
- final VisibilityChecker rejectChecker =
- (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+ final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -4952,15 +5290,15 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- rejectChecker);
+ rejectChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema type
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5011,7 +5349,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5037,7 +5375,7 @@
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build(),
new AppSearchSchema.Builder("Type3").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5062,7 +5400,7 @@
ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5082,7 +5420,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5115,7 +5453,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5143,7 +5481,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 1,
/*setSchemaStatsBuilder=*/ null);
@@ -5170,7 +5508,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 2,
/*setSchemaStatsBuilder=*/ null);
@@ -5207,7 +5545,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5241,7 +5579,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5262,16 +5600,30 @@
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;
+ final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
+ return false;
+ }
+
+ for (PackageIdentifier packageIdentifier :
+ visibilityStore.getVisibility(prefixedSchema)
+ .getVisibilityConfig().getAllowedPackages()) {
+ if (packageIdentifier.getPackageName().equals(fakeListeningPackage)) {
+ return true;
}
- Set<String> allowedPackages = new ArraySet<>(
- visibilityStore.getVisibility(prefixedSchema).getPackageNames());
- return allowedPackages.contains(fakeListeningPackage);
- };
+ }
+ return false;
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -5280,8 +5632,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- visibilityChecker);
+ visibilityChecker, ALWAYS_OPTIMIZE
+ );
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
@@ -5300,16 +5652,15 @@
mContext.getPackageName(),
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(
- new VisibilityDocument.Builder("Type1")
+ /*visibilityConfigs=*/ ImmutableList.of(
+ new InternalVisibilityConfig.Builder("Type1")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
.build(),
- new VisibilityDocument.Builder("Type2")
+ new InternalVisibilityConfig.Builder("Type2")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
- .build()
- ),
+ .build()),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5330,12 +5681,12 @@
mContext.getPackageName(),
"database1",
schemas,
- /*visibilityDocuments=*/ ImmutableList.of(
- new VisibilityDocument.Builder("Type1")
+ /*visibilityConfigs=*/ ImmutableList.of(
+ new InternalVisibilityConfig.Builder("Type1")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
.build(),
- new VisibilityDocument.Builder("Type2").build()
+ new InternalVisibilityConfig.Builder("Type2").build()
),
/*forceOverride=*/ false,
/*version=*/ 0,
@@ -5365,13 +5716,12 @@
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build()),
- /*visibilityDocuments=*/ ImmutableList.of(
- new VisibilityDocument.Builder("Type1")
+ /*visibilityConfigs=*/ ImmutableList.of(
+ new InternalVisibilityConfig.Builder("Type1")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
.build(),
- new VisibilityDocument.Builder("Type2").build()
- ),
+ new InternalVisibilityConfig.Builder("Type2").build()),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5396,16 +5746,15 @@
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build()),
- /*visibilityDocuments=*/ImmutableList.of(
- new VisibilityDocument.Builder("Type1")
+ /*visibilityConfigs=*/ImmutableList.of(
+ new InternalVisibilityConfig.Builder("Type1")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
.build(),
- new VisibilityDocument.Builder("Type2")
+ new InternalVisibilityConfig.Builder("Type2")
.addVisibleToPackage(
new PackageIdentifier(fakeListeningPackage, new byte[0]))
- .build()
- ),
+ .build()),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5426,10 +5775,20 @@
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)
+ final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
&& prefixedSchema.endsWith("Type2");
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -5438,8 +5797,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- visibilityChecker);
+ visibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5460,7 +5819,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5494,7 +5853,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
.build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5515,10 +5874,20 @@
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)
+ final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
&& prefixedSchema.endsWith("Type2");
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -5527,8 +5896,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- visibilityChecker);
+ visibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5537,7 +5906,7 @@
ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5557,7 +5926,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5575,7 +5944,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5597,21 +5966,29 @@
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);
- }
- };
+ final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+ @NonNull String packageName, @NonNull String prefixedSchema,
+ @NonNull VisibilityStore 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);
+ }
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+ return false;
+ }
+ };
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
@@ -5620,8 +5997,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- visibilityChecker);
+ visibilityChecker, ALWAYS_OPTIMIZE
+ );
// Add a schema.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5633,7 +6010,7 @@
new AppSearchSchema.Builder("Type3").build(),
new AppSearchSchema.Builder("Type4").build()
),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5669,7 +6046,7 @@
mContext.getPackageName(),
"database1",
ImmutableList.of(),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 0,
/*setSchemaStatsBuilder=*/ null);
@@ -5724,7 +6101,7 @@
.build()
).build()
),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 1,
/*setSchemaStatsBuilder=*/ null);
@@ -5761,7 +6138,7 @@
mContext.getPackageName(),
"database1",
updatedSchemaTypes,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ false,
/*version=*/ 2,
/*setSchemaStatsBuilder=*/ null);
@@ -5783,7 +6160,7 @@
mContext.getPackageName(),
"database1",
updatedSchemaTypes,
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
/*version=*/ 3,
/*setSchemaStatsBuilder=*/ null);
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 e9b8e47..aa4c96a 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
@@ -80,8 +80,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
mLogger = new SimpleTestLogger();
}
@@ -373,8 +373,8 @@
new LocalStorageIcingOptionsConfig()
),
initStatsBuilder,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
InitializeStats iStats = initStatsBuilder.build();
appSearchImpl.close();
@@ -406,8 +406,8 @@
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build());
@@ -443,7 +443,7 @@
appSearchImpl = AppSearchImpl.create(
folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()),
- initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+ initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
InitializeStats iStats = initStatsBuilder.build();
assertThat(iStats).isNotNull();
@@ -456,7 +456,8 @@
assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
assertThat(iStats.getDocumentCount()).isEqualTo(2);
- assertThat(iStats.getSchemaTypeCount()).isEqualTo(4); // +2 for VisibilitySchema
+ // Type1 + Type2 +2 for VisibilitySchema, +1 for VisibilityOverlay
+ assertThat(iStats.getSchemaTypeCount()).isEqualTo(5);
assertThat(iStats.hasReset()).isEqualTo(false);
assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
appSearchImpl.close();
@@ -471,7 +472,7 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(
folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
@@ -511,7 +512,7 @@
appSearchImpl = AppSearchImpl.create(
folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()),
- initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+ initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
InitializeStats iStats = initStatsBuilder.build();
// Some of other fields are already covered by AppSearchImplTest#testReset()
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java
new file mode 100644
index 0000000..fadf4bf
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 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;
+
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public class SchemaCacheTest {
+
+ @Test
+ public void testGetSchemaTypesWithDescendants() throws Exception {
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto personSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Person")
+ .build();
+ SchemaTypeConfigProto artistSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Artist")
+ .addParentTypes("package$database/Person")
+ .build();
+ SchemaTypeConfigProto otherSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Other")
+ .build();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/Person", personSchema,
+ "package$database/Artist", artistSchema,
+ "package$database/Other", otherSchema));
+ SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/Person"))).containsExactly(
+ "package$database/Person", "package$database/Artist");
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/Artist"))).containsExactly(
+ "package$database/Artist");
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/Other"))).containsExactly(
+ "package$database/Other");
+ }
+
+ @Test
+ public void testGetSchemaTypesWithDescendants_multipleLevel() throws Exception {
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto schemaA =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/A")
+ .build();
+ SchemaTypeConfigProto schemaB =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/B")
+ .build();
+ SchemaTypeConfigProto schemaC =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/C")
+ .addParentTypes("package$database/A")
+ .build();
+ SchemaTypeConfigProto schemaD =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/D")
+ .addParentTypes("package$database/C")
+ .build();
+ SchemaTypeConfigProto schemaE =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/E")
+ .addParentTypes("package$database/B")
+ .addParentTypes("package$database/C")
+ .build();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/A", schemaA,
+ "package$database/B", schemaB,
+ "package$database/C", schemaC,
+ "package$database/D", schemaD,
+ "package$database/E", schemaE));
+ SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/A"))).containsExactly(
+ "package$database/A", "package$database/C", "package$database/D",
+ "package$database/E");
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/B"))).containsExactly(
+ "package$database/B", "package$database/E");
+ assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+ ImmutableSet.of("package$database/A", "package$database/B"))).containsExactly(
+ "package$database/A", "package$database/B", "package$database/C",
+ "package$database/D", "package$database/E");
+ }
+}
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 0690cd5..f189baf 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
@@ -55,8 +55,8 @@
new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()
),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
}
@After
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index a8bb683..ad33c41 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -33,6 +34,7 @@
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -44,6 +46,10 @@
private static final byte[] BYTE_ARRAY_2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
private static final String SCHEMA_TYPE_1 = "sDocumentPropertiesSchemaType1";
private static final String SCHEMA_TYPE_2 = "sDocumentPropertiesSchemaType2";
+ private static final EmbeddingVector sEmbedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ private static final EmbeddingVector sEmbedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
private static final GenericDocument DOCUMENT_PROPERTIES_1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>(
"namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
@@ -429,4 +435,59 @@
expectedDocWithParentAsMetaField);
}
// @exportToFramework:endStrip()
+
+ @Test
+ public void testDocumentProtoConvert_EmbeddingProperty() throws Exception {
+ GenericDocument document =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+ SCHEMA_TYPE_1)
+ .setCreationTimestampMillis(5L)
+ .setScore(1)
+ .setTtlMillis(1L)
+ .setPropertyLong("longKey1", 1L)
+ .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES_1)
+ .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+ .build();
+
+ // Create the Document proto. Need to sort the property order by key.
+ DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+ .setUri("id1")
+ .setSchema(SCHEMA_TYPE_1)
+ .setCreationTimestampMs(5L)
+ .setScore(1)
+ .setTtlMs(1L)
+ .setNamespace("namespace");
+ HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+ propertyProtoMap.put("longKey1",
+ PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+ propertyProtoMap.put("documentKey1",
+ PropertyProto.newBuilder().setName("documentKey1").addDocumentValues(
+ GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_1)));
+ propertyProtoMap.put("embeddingKey1",
+ PropertyProto.newBuilder().setName("embeddingKey1")
+ .addVectorValues(PropertyProto.VectorProto.newBuilder()
+ .addAllValues(Arrays.asList(1.1f, 2.2f, 3.3f))
+ .setModelSignature("my_model_v1")
+ )
+ .addVectorValues(PropertyProto.VectorProto.newBuilder()
+ .addAllValues(Arrays.asList(4.4f, 5.5f, 6.6f, 7.7f))
+ .setModelSignature("my_model_v2")
+ ));
+ List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
+ Collections.sort(sortedKey);
+ for (String key : sortedKey) {
+ documentProtoBuilder.addProperties(propertyProtoMap.get(key));
+ }
+ DocumentProto documentProto = documentProtoBuilder.build();
+
+ GenericDocument convertedGenericDocument =
+ GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+ SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new LocalStorageIcingOptionsConfig()));
+ DocumentProto convertedDocumentProto =
+ GenericDocumentToProtoConverter.toDocumentProto(document);
+
+ assertThat(convertedDocumentProto).isEqualTo(documentProto);
+ assertThat(convertedGenericDocument).isEqualTo(document);
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 4513357..bcef397 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -21,6 +21,7 @@
import androidx.appsearch.app.AppSearchSchema;
import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
import com.google.android.icing.proto.JoinableConfig;
import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -33,6 +34,128 @@
public class SchemaToProtoConverterTest {
@Test
+ public void testGetProto_DescriptionSet() {
+ AppSearchSchema emailSchema =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message.")
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setDescription("The most important part.")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig
+ .INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig
+ .TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("timestamp")
+ .setDescription("The time at which the email was sent.")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DoublePropertyConfig.Builder("importanceScore")
+ .setDescription(
+ "A value representing this document's importance.")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder("read")
+ .setDescription(
+ "Whether the email has been read by the recipient")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BytesPropertyConfig.Builder("attachment")
+ .setDescription("Documents that are attached to the email.")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ // We don't need to actually define the Person type for this test because
+ // the converter will process each schema individually.
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "sender", "Person")
+ .setDescription("The person who wrote this email.")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+
+ SchemaTypeConfigProto expectedEmailProto =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("Email")
+ .setDescription("A type of electronic message.")
+ .setVersion(12345)
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("subject")
+ .setDescription("The most important part.")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setStringIndexingConfig(
+ StringIndexingConfig.newBuilder()
+ .setTokenizerType(
+ StringIndexingConfig.TokenizerType
+ .Code.PLAIN)
+ .setTermMatchType(
+ TermMatchType.Code.PREFIX)))
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("timestamp")
+ .setDescription("The time at which the email was sent.")
+ .setDataType(PropertyConfigProto.DataType.Code.INT64)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.OPTIONAL))
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("importanceScore")
+ .setDescription(
+ "A value representing this document's importance.")
+ .setDataType(PropertyConfigProto.DataType.Code.DOUBLE)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.OPTIONAL))
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("read")
+ .setDescription(
+ "Whether the email has been read by the recipient")
+ .setDataType(PropertyConfigProto.DataType.Code.BOOLEAN)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.OPTIONAL))
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("attachment")
+ .setDescription("Documents that are attached to the email.")
+ .setDataType(PropertyConfigProto.DataType.Code.BYTES)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.REPEATED))
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("sender")
+ .setSchemaType("Person")
+ .setDescription("The person who wrote this email.")
+ .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+ .setCardinality(
+ PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setDocumentIndexingConfig(
+ DocumentIndexingConfig.newBuilder()
+ .setIndexNestedProperties(false)))
+ .build();
+
+ assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/ 12345))
+ .isEqualTo(expectedEmailProto);
+ assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+ .isEqualTo(emailSchema);
+ }
+
+ @Test
public void testGetProto_Email() {
AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
@@ -53,9 +176,11 @@
SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
.setSchemaType("Email")
+ .setDescription("")
.setVersion(12345)
.addProperties(PropertyConfigProto.newBuilder()
.setPropertyName("subject")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.STRING)
.setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
.setStringIndexingConfig(
@@ -66,6 +191,7 @@
)
).addProperties(PropertyConfigProto.newBuilder()
.setPropertyName("body")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.STRING)
.setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
.setStringIndexingConfig(
@@ -99,9 +225,11 @@
SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder()
.setSchemaType("MusicRecording")
+ .setDescription("")
.setVersion(0)
.addProperties(PropertyConfigProto.newBuilder()
.setPropertyName("artist")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.STRING)
.setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
.setStringIndexingConfig(
@@ -112,6 +240,7 @@
)
).addProperties(PropertyConfigProto.newBuilder()
.setPropertyName("pubDate")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.INT64)
.setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
).build();
@@ -130,28 +259,21 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setJoinableValueType(AppSearchSchema.StringPropertyConfig
.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- // TODO(b/274157614): Export this to framework when we can access hidden
- // APIs.
- // @exportToFramework:startStrip()
- // TODO(b/274157614) start exporting this when it is unhidden in framework
- .setDeletionPropagation(true)
- // @exportToFramework:endStrip()
.build()
).build();
JoinableConfig joinableConfig = JoinableConfig.newBuilder()
.setValueType(JoinableConfig.ValueType.Code.QUALIFIED_ID)
- // @exportToFramework:startStrip()
- .setPropagateDelete(true)
- // @exportToFramework:endStrip()
.build();
SchemaTypeConfigProto expectedAlbumProto = SchemaTypeConfigProto.newBuilder()
.setSchemaType("Album")
+ .setDescription("")
.setVersion(0)
.addProperties(
PropertyConfigProto.newBuilder()
.setPropertyName("artist")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.STRING)
.setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
.setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -176,6 +298,7 @@
SchemaTypeConfigProto expectedSchemaProto = SchemaTypeConfigProto.newBuilder()
.setSchemaType("EmailMessage")
+ .setDescription("")
.setVersion(12345)
.addParentTypes("Email")
.addParentTypes("Message")
@@ -212,10 +335,12 @@
SchemaTypeConfigProto expectedPersonProto = SchemaTypeConfigProto.newBuilder()
.setSchemaType("Person")
+ .setDescription("")
.setVersion(0)
.addProperties(
PropertyConfigProto.newBuilder()
.setPropertyName("name")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.STRING)
.setCardinality(PropertyConfigProto.Cardinality.Code.REQUIRED)
.setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -225,6 +350,7 @@
.addProperties(
PropertyConfigProto.newBuilder()
.setPropertyName("worksFor")
+ .setDescription("")
.setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
.setSchemaType("Organization")
.setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -236,4 +362,89 @@
assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedPersonProto))
.isEqualTo(personSchema);
}
+
+ @Test
+ public void testGetProto_EmbeddingProperty() {
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig
+ .INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("indexableEmbedding")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig
+ .INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+
+ SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("Email")
+ .setDescription("")
+ .setVersion(12345)
+ .addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("subject")
+ .setDescription("")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setStringIndexingConfig(
+ StringIndexingConfig.newBuilder()
+ .setTokenizerType(
+ StringIndexingConfig.TokenizerType.Code.PLAIN)
+ .setTermMatchType(TermMatchType.Code.PREFIX)
+ )
+ ).addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("body")
+ .setDescription("")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setStringIndexingConfig(
+ StringIndexingConfig.newBuilder()
+ .setTokenizerType(
+ StringIndexingConfig.TokenizerType.Code.PLAIN)
+ .setTermMatchType(TermMatchType.Code.PREFIX)
+ )
+ ).addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("embedding")
+ .setDescription("")
+ .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ ).addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("indexableEmbedding")
+ .setDescription("")
+ .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setEmbeddingIndexingConfig(
+ EmbeddingIndexingConfig.newBuilder()
+ .setEmbeddingIndexingType(
+ EmbeddingIndexingConfig.EmbeddingIndexingType.Code
+ .LINEAR_SEARCH)
+ )
+ ).build();
+
+ assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/12345))
+ .isEqualTo(expectedEmailProto);
+ assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+ .isEqualTo(emailSchema);
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index ccf3b51..9ce2a32 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -27,6 +27,7 @@
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -84,7 +85,7 @@
removePrefixesFromDocument(documentProtoBuilder);
removePrefixesFromDocument(joinedDocProtoBuilder);
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
- searchResultProto, schemaMap, config);
+ searchResultProto, new SchemaCache(schemaMap), config);
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult result = searchResultPage.getResults().get(0);
assertThat(result.getPackageName()).isEqualTo("com.package.foo");
@@ -148,7 +149,8 @@
removePrefixesFromDocument(documentProtoBuilder);
Exception e = assertThrows(AppSearchException.class,
- () -> SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap,
+ () -> SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
+ new SchemaCache(schemaMap),
new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig())));
assertThat(e.getMessage())
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 778ab7a..dbb8f58 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
@@ -30,6 +30,7 @@
import androidx.appsearch.localstorage.IcingOptionsConfig;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
@@ -78,8 +79,8 @@
mLocalStorageIcingOptionsConfig
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
}
@After
@@ -105,13 +106,13 @@
prefix2, ImmutableSet.of(
prefix2 + "namespace1",
prefix2 + "namespace2")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- prefix1 + "typeA", configProto,
- prefix1 + "typeB", configProto),
- prefix2, ImmutableMap.of(
- prefix2 + "typeA", configProto,
- prefix2 + "typeB", configProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto))),
mLocalStorageIcingOptionsConfig);
// Convert SearchSpec to proto.
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -155,13 +156,13 @@
prefix2, ImmutableSet.of(
prefix2 + "namespace1",
prefix2 + "namespace2")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- prefix1 + "typeA", configProto,
- prefix1 + "typeB", configProto),
- prefix2, ImmutableMap.of(
- prefix2 + "typeA", configProto,
- prefix2 + "typeB", configProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto))),
mLocalStorageIcingOptionsConfig);
// Convert SearchSpec to proto.
@@ -227,15 +228,15 @@
prefix2,
ImmutableSet.of(
prefix2 + "namespace1", prefix2 + "namespace2")),
- /*schemaMap=*/ ImmutableMap.of(
- prefix1,
- ImmutableMap.of(
- prefix1 + "typeA", configProto,
- prefix1 + "typeB", configProto),
- prefix2,
- ImmutableMap.of(
- prefix2 + "typeA", configProto,
- prefix2 + "typeB", configProto)),
+ new SchemaCache(/*schemaMap=*/ ImmutableMap.of(
+ prefix1,
+ ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2,
+ ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto))),
mLocalStorageIcingOptionsConfig);
VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
@@ -287,8 +288,9 @@
/*queryExpression=*/"",
searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(prefix, ImmutableSet.of(prefix + namespace)),
- /*schemaMap=*/ImmutableMap.of(prefix, ImmutableMap.of(prefix + schemaType,
- SchemaTypeConfigProto.getDefaultInstance())),
+ new SchemaCache(/*schemaMap=*/
+ ImmutableMap.of(prefix, ImmutableMap.of(prefix + schemaType,
+ SchemaTypeConfigProto.getDefaultInstance()))),
mLocalStorageIcingOptionsConfig).toScoringSpecProto();
TypePropertyWeights typePropertyWeights = TypePropertyWeights.newBuilder()
.setSchemaType(prefix + schemaType)
@@ -317,7 +319,7 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig).toScoringSpecProto();
assertThat(scoringSpecProto.getOrderBy().getNumber())
@@ -342,11 +344,11 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = convert.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -380,12 +382,12 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -428,12 +430,12 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
namespaceMap,
- schemaMap);
+ new SchemaCache(schemaMap));
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(1);
assertThat(resultSpecProto.getResultGroupings(0).getEntryGroupings(0).getNamespace())
@@ -479,12 +481,12 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
namespaceMap,
- schemaMap);
+ new SchemaCache(schemaMap));
assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -507,7 +509,7 @@
String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
SearchSpec searchSpec = new SearchSpec.Builder()
- .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
.build();
SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -515,12 +517,12 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -534,7 +536,7 @@
@Test
public void testToResultSpecProto_projectionNoPrefixes_withWildcard() {
SearchSpec searchSpec = new SearchSpec.Builder()
- .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
.build();
SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -542,12 +544,12 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -555,8 +557,59 @@
assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
}
- // @exportToFramework:startStrip()
- // TODO(b/274157614): Export this to framework when property filters are made public
+ @Test
+ public void testToResultSpecProto_projection_removeSchemaWithoutParentInFilter() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("Person")
+ .addProjection("Artist", ImmutableList.of("name"))
+ .addProjection("Other", ImmutableList.of("email"))
+ .build();
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto personSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Person")
+ .build();
+ SchemaTypeConfigProto artistSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Artist")
+ .addParentTypes("package$database/Person")
+ .build();
+ SchemaTypeConfigProto otherSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Other")
+ .build();
+
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/Person", personSchema,
+ "package$database/Artist", artistSchema,
+ "package$database/Other", otherSchema));
+ Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace"));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/namespaceMap,
+ new SchemaCache(schemaMap),
+ mLocalStorageIcingOptionsConfig);
+
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ namespaceMap,
+ new SchemaCache(schemaMap));
+
+ // The "name" property specified in Artist's projection should remain in the result,
+ // since even though Artist doesn't exist in the original schema filters directly, we have
+ // specified its parent, Person, in the schema filters.
+ // The "email" property specified in Other's projection should be dropped as usual.
+ assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
+ assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
+ "package$database/Artist");
+ assertThat(resultSpecProto.getTypePropertyMasks(0).getPathsCount()).isEqualTo(1);
+ assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
+ }
+
@Test
public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() {
String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -591,7 +644,7 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -609,7 +662,6 @@
assertThat(nestedSearchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("type");
}
- // TODO(b/274157614): Export this to framework when property filters are made public
@Test
public void testToSearchSpecProto_propertyFilter_withWildcard() {
String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -624,7 +676,7 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -635,7 +687,57 @@
assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
}
- // @exportToFramework:endStrip()
+ @Test
+ public void testToSearchSpecProto_propertyFilter_removeSchemaWithoutParentInFilter() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("Person")
+ .addFilterProperties("Artist", ImmutableList.of("name"))
+ .addFilterProperties("Other", ImmutableList.of("email"))
+ .build();
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto personSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Person")
+ .build();
+ SchemaTypeConfigProto artistSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Artist")
+ .addParentTypes("package$database/Person")
+ .build();
+ SchemaTypeConfigProto otherSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Other")
+ .build();
+
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/Person", personSchema,
+ "package$database/Artist", artistSchema,
+ "package$database/Other", otherSchema));
+ Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace"));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/namespaceMap,
+ new SchemaCache(schemaMap),
+ mLocalStorageIcingOptionsConfig);
+
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+ // The "name" property specified in Artist's property filters should remain in the result,
+ // since even though Artist doesn't exist in the original schema filters directly, we have
+ // specified its parent, Person, in the schema filters.
+ // The "email" property specified in Other's property filters should be dropped as usual.
+ assertThat(searchSpecProto.getTypePropertyFiltersCount()).isEqualTo(1);
+ assertThat(searchSpecProto.getTypePropertyFilters(0).getSchemaType()).isEqualTo(
+ "package$database/Artist");
+ assertThat(searchSpecProto.getTypePropertyFilters(0).getPathsCount()).isEqualTo(1);
+ assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
+ }
+
@Test
public void testToResultSpecProto_weight_withJoinSpec_packageFilter() throws Exception {
String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -671,7 +773,7 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
ScoringSpecProto scoringSpecProto = converter.toScoringSpecProto();
@@ -709,7 +811,7 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(
@@ -719,7 +821,7 @@
prefix2, ImmutableSet.of(
prefix2 + "namespaceA",
prefix2 + "namespaceB")),
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
// First grouping should have same package name.
@@ -762,11 +864,11 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap,
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
namespaceMap,
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
// First grouping should have same namespace.
@@ -786,8 +888,6 @@
PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getNamespace()));
}
- // @exportToFramework:startStrip()
- // TODO(b/258715421) start exporting this when it is unhidden in framework
@Test
public void testToResultSpecProto_groupBySchema() throws Exception {
SearchSpec searchSpec = new SearchSpec.Builder()
@@ -811,11 +911,11 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
/*namespaceMap=*/ImmutableMap.of(),
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- schemaMap);
+ new SchemaCache(schemaMap));
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
// First grouping should have the same schema type.
@@ -834,7 +934,6 @@
.isEqualTo(
PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getSchema()));
}
- // @exportToFramework:endStrip()
@Test
public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
@@ -857,11 +956,12 @@
/*queryExpression=*/"query",
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
- namespaceMap, /*schemaMap=*/ImmutableMap.of(),
+ namespaceMap,
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
namespaceMap,
- /*schemaMap=*/ImmutableMap.of());
+ new SchemaCache());
// All namespace should be separated.
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -871,8 +971,6 @@
assertThat(resultSpecProto.getResultGroupings(3).getEntryGroupingsList()).hasSize(1);
}
- // @exportToFramework:startStrip()
- // TODO(b/258715421) start exporting this when it is unhidden in framework
@Test
public void testToResultSpecProto_groupBySchemaAndPackage() throws Exception {
SearchSpec searchSpec = new SearchSpec.Builder()
@@ -896,11 +994,11 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
/*namespaceMap=*/ImmutableMap.of(),
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
ResultSpecProto resultSpecProto = converter.toResultSpecProto(
/*namespaceMap=*/ImmutableMap.of(),
- schemaMap);
+ new SchemaCache(schemaMap));
// All schema should be separated.
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -940,9 +1038,10 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
- ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap,
+ new SchemaCache(schemaMap));
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1032,9 +1131,10 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap,
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
- ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap,
+ new SchemaCache(schemaMap));
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(8);
ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1110,7 +1210,6 @@
assertThat(grouping8.getEntryGroupings(0).getSchema())
.isEqualTo("package1$database/typeB");
}
- // @exportToFramework:endStrip()
@Test
public void testGetTargetNamespaceFilters_emptySearchingFilter() {
@@ -1127,7 +1226,8 @@
/*queryExpression=*/"",
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
- namespaceMap, /*schemaMap=*/ImmutableMap.of(),
+ namespaceMap,
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1153,7 +1253,7 @@
"package$database1/namespace2"),
prefix2, ImmutableSet.of("package$database2/namespace3",
"package$database2/namespace4")),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1176,7 +1276,7 @@
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1",
"package$database1/namespace2")),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1200,7 +1300,7 @@
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1",
"package$database1/namespace2")),
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1222,13 +1322,13 @@
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- "package$database1/typeA", schemaTypeConfigProto,
- "package$database1/typeB", schemaTypeConfigProto),
- prefix2, ImmutableMap.of(
- "package$database2/typeC", schemaTypeConfigProto,
- "package$database2/typeD", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto),
+ prefix2, ImmutableMap.of(
+ "package$database2/typeC", schemaTypeConfigProto,
+ "package$database2/typeD", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// Empty searching filter will get all types for target filter
@@ -1251,13 +1351,13 @@
/*prefixes=*/ImmutableSet.of(prefix1),
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- "package$database1/typeA", schemaTypeConfigProto,
- "package$database1/typeB", schemaTypeConfigProto),
- prefix2, ImmutableMap.of(
- "package$database2/typeC", schemaTypeConfigProto,
- "package$database2/typeD", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto),
+ prefix2, ImmutableMap.of(
+ "package$database2/typeC", schemaTypeConfigProto,
+ "package$database2/typeD", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// Only search prefix1 will return typeA and B.
@@ -1279,10 +1379,10 @@
/*prefixes=*/ImmutableSet.of(prefix1),
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- "package$database1/typeA", schemaTypeConfigProto,
- "package$database1/typeB", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// If the searching schema filter is not empty, the target schema filter will be the
@@ -1293,6 +1393,96 @@
}
@Test
+ public void testGetTargetSchemaFilters_polymorphismExpansion() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("Person", "nonExist").build();
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto personSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Person")
+ .build();
+ SchemaTypeConfigProto artistSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Artist")
+ .addParentTypes("package$database/Person")
+ .build();
+ SchemaTypeConfigProto otherSchema =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/Other")
+ .build();
+
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/Person", personSchema,
+ "package$database/Artist", artistSchema,
+ "package$database/Other", otherSchema));
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace")),
+ new SchemaCache(schemaMap),
+ mLocalStorageIcingOptionsConfig);
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+ // The schema filter of "Person" specified in searchSpec will be expanded to "Artist" via
+ // polymorphism.
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database/Person", "package$database/Artist");
+ }
+
+ @Test
+ public void testGetTargetSchemaFilters_polymorphismExpansion_multipleLevel() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("A", "B").build();
+ String prefix = createPrefix("package", "database");
+ SchemaTypeConfigProto schemaA =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/A")
+ .build();
+ SchemaTypeConfigProto schemaB =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/B")
+ .build();
+ SchemaTypeConfigProto schemaC =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/C")
+ .addParentTypes("package$database/A")
+ .build();
+ SchemaTypeConfigProto schemaD =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/D")
+ .addParentTypes("package$database/C")
+ .build();
+ SchemaTypeConfigProto schemaE =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("package$database/E")
+ .addParentTypes("package$database/B")
+ .addParentTypes("package$database/C")
+ .build();
+
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/A", schemaA,
+ "package$database/B", schemaB,
+ "package$database/C", schemaC,
+ "package$database/D", schemaD,
+ "package$database/E", schemaE));
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace")),
+ new SchemaCache(schemaMap),
+ mLocalStorageIcingOptionsConfig);
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database/A", "package$database/B", "package$database/C",
+ "package$database/D", "package$database/E");
+ }
+
+ @Test
public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
// Put non-exist searching schema.
SearchSpec searchSpec = new SearchSpec.Builder()
@@ -1306,10 +1496,10 @@
/*prefixes=*/ImmutableSet.of(prefix1),
/*namespaceMap=*/ImmutableMap.of(
prefix1, ImmutableSet.of("package$database1/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- "package$database1/typeA", schemaTypeConfigProto,
- "package$database1/typeB", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
// If there is no intersection of the schema filters that user want to search over and
@@ -1335,11 +1525,11 @@
/*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(
prefix, ImmutableSet.of("package$database/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix, ImmutableMap.of(
- "package$database/schema1", schemaTypeConfigProto,
- "package$database/schema2", schemaTypeConfigProto,
- "package$database/schema3", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/schema1", schemaTypeConfigProto,
+ "package$database/schema2", schemaTypeConfigProto,
+ "package$database/schema3", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
converter.removeInaccessibleSchemaFilter(
@@ -1386,7 +1576,7 @@
/*queryExpression=*/"",
searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/namespaceMap,
- /*schemaMap=*/ImmutableMap.of(),
+ new SchemaCache(),
mLocalStorageIcingOptionsConfig);
assertThat(emptySchemaConverter.hasNothingToSearch()).isTrue();
@@ -1395,7 +1585,7 @@
/*queryExpression=*/"",
searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(),
- schemaMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
assertThat(emptyNamespaceConverter.hasNothingToSearch()).isTrue();
@@ -1403,7 +1593,8 @@
new SearchSpecToProtoConverter(
/*queryExpression=*/"",
searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
- namespaceMap, schemaMap,
+ namespaceMap,
+ new SchemaCache(schemaMap),
mLocalStorageIcingOptionsConfig);
assertThat(nonEmptyConverter.hasNothingToSearch()).isFalse();
@@ -1437,11 +1628,11 @@
/*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(
prefix, ImmutableSet.of("package$database/namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix, ImmutableMap.of(
- "package$database/schema1", schemaTypeConfigProto,
- "package$database/schema2", schemaTypeConfigProto,
- "package$database/schema3", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/schema1", schemaTypeConfigProto,
+ "package$database/schema2", schemaTypeConfigProto,
+ "package$database/schema3", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
converter.removeInaccessibleSchemaFilter(
@@ -1489,7 +1680,7 @@
/*queryExpression=*/"",
searchSpec, /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap,
- schemaTypeMap,
+ new SchemaCache(schemaTypeMap),
mLocalStorageIcingOptionsConfig);
TypePropertyWeights expectedTypePropertyWeight1 =
@@ -1538,9 +1729,9 @@
/*namespaceMap=*/ImmutableMap.of(
prefix1,
ImmutableSet.of(prefix1 + "namespace1")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1,
- ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)),
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1,
+ ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto))),
mLocalStorageIcingOptionsConfig);
ScoringSpecProto convertedScoringSpecProto = converter.toScoringSpecProto();
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
index f918784..365d403 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.appsearch.localstorage.util.PrefixUtil;
import com.google.android.icing.proto.NamespaceDocumentUriGroup;
@@ -54,10 +55,10 @@
prefix1, ImmutableSet.of(
prefix1 + "namespace1",
prefix1 + "namespace2")),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- prefix1 + "typeA", configProto,
- prefix1 + "typeB", configProto)));
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto))));
SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
@@ -74,9 +75,6 @@
.build());
}
- // @exportToFramework:startStrip()
- // TODO(b/230553264) remove this when it is deprecated and replaced by
- // advanced query property filters or it is exportable.
@Test
public void testToProto_propertyFilters() throws Exception {
SearchSuggestionSpec searchSuggestionSpec =
@@ -91,10 +89,10 @@
searchSuggestionSpec,
/*prefixes=*/ImmutableSet.of(prefix1),
/*namespaceMap=*/ImmutableMap.of(),
- /*schemaMap=*/ImmutableMap.of(
- prefix1, ImmutableMap.of(
- prefix1 + "typeA", configProto,
- prefix1 + "typeB", configProto)));
+ new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto))));
SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
assertThat(proto.getTypePropertyFiltersList()).containsExactly(
@@ -103,5 +101,4 @@
.addPaths("property1").addPaths("property2")
.build());
}
- // @exportToFramework:endStrip()
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index fbc9a93..ad86e02 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -23,6 +23,7 @@
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -95,7 +96,7 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()));
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -136,7 +137,7 @@
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()));
assertThat(searchResultPage.getResults()).hasSize(1);
assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
@@ -193,7 +194,7 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()));
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -280,7 +281,7 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()));
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
index 667dd86..c86d4cc 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -227,6 +227,7 @@
int nativeLockAcquisitionLatencyMillis = 20;
int javaToNativeJniLatencyMillis = 21;
int nativeToJavaJniLatencyMillis = 22;
+ String searchSourceLogTag = "tag";
final SearchStats.Builder sStatsBuilder = new SearchStats.Builder(visibilityScope,
TEST_PACKAGE_NAME)
.setDatabase(TEST_DATA_BASE)
@@ -253,7 +254,8 @@
.setDocumentRetrievingLatencyMillis(nativeDocumentRetrievingLatencyMillis)
.setNativeLockAcquisitionLatencyMillis(nativeLockAcquisitionLatencyMillis)
.setJavaToNativeJniLatencyMillis(javaToNativeJniLatencyMillis)
- .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis);
+ .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis)
+ .setSearchSourceLogTag(searchSourceLogTag);
final SearchStats sStats = sStatsBuilder.build();
assertThat(sStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
@@ -295,7 +297,7 @@
javaToNativeJniLatencyMillis);
assertThat(sStats.getNativeToJavaJniLatencyMillis()).isEqualTo(
nativeToJavaJniLatencyMillis);
-
+ assertThat(sStats.getSearchSourceLogTag()).isEqualTo(searchSourceLogTag);
}
@Test
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
new file mode 100644
index 0000000..f8a941d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ClickStatsTest {
+ @Test
+ public void testBuilder() {
+ long timestampMillis = 1L;
+ long timeStayOnResultMillis = 2L;
+ int resultRankInBlock = 3;
+ int resultRankGlobal = 4;
+ boolean isGoodClick = false;
+
+ final ClickStats clickStats =
+ new ClickStats.Builder()
+ .setTimestampMillis(timestampMillis)
+ .setTimeStayOnResultMillis(timeStayOnResultMillis)
+ .setResultRankInBlock(resultRankInBlock)
+ .setResultRankGlobal(resultRankGlobal)
+ .setIsGoodClick(isGoodClick)
+ .build();
+
+ assertThat(clickStats.getTimestampMillis()).isEqualTo(timestampMillis);
+ assertThat(clickStats.getTimeStayOnResultMillis()).isEqualTo(timeStayOnResultMillis);
+ assertThat(clickStats.getResultRankInBlock()).isEqualTo(resultRankInBlock);
+ assertThat(clickStats.getResultRankGlobal()).isEqualTo(resultRankGlobal);
+ assertThat(clickStats.isGoodClick()).isEqualTo(isGoodClick);
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
new file mode 100644
index 0000000..66e2ed4
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2024 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchIntentStatsTest {
+ static final String TEST_PACKAGE_NAME = "package.test";
+ static final String TEST_DATA_BASE = "testDataBase";
+
+ @Test
+ public void testBuilder() {
+ String prevQuery = "prev";
+ String currQuery = "curr";
+ long searchIntentTimestampMillis = 1L;
+ int numResultsFetched = 2;
+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+ // Clicks associated with the search intent.
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+
+ final SearchIntentStats searchIntentStats =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery(prevQuery)
+ .setCurrQuery(currQuery)
+ .setTimestampMillis(searchIntentTimestampMillis)
+ .setNumResultsFetched(numResultsFetched)
+ .setQueryCorrectionType(queryCorrectionType)
+ .addClicksStats(clickStats0, clickStats1)
+ .build();
+
+ assertThat(searchIntentStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats.getClicksStats()).containsExactly(clickStats0, clickStats1);
+ }
+
+ @Test
+ public void testBuilderCopy_allFieldsAreCopied() {
+ String prevQuery = "prev";
+ String currQuery = "curr";
+ long searchIntentTimestampMillis = 1L;
+ int numResultsFetched = 2;
+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+ // Clicks associated with the search intent.
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery(prevQuery)
+ .setCurrQuery(currQuery)
+ .setTimestampMillis(searchIntentTimestampMillis)
+ .setNumResultsFetched(numResultsFetched)
+ .setQueryCorrectionType(queryCorrectionType)
+ .addClicksStats(clickStats0, clickStats1)
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(searchIntentStats0).build();
+
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats1.getClicksStats()).containsExactly(clickStats0, clickStats1);
+ }
+
+ @Test
+ public void testBuilderCopy_copiedFieldsCanBeUpdated() {
+ // Clicks associated with the search intent.
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+ .addClicksStats(clickStats0, clickStats1)
+ .build();
+
+ // Build another SearchIntentStats based on the previous one, with fields changed.
+ final ClickStats clickStats2 =
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(searchIntentStats0)
+ .setDatabase("database2")
+ .setPrevQuery("query3")
+ .setCurrQuery("query4")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(clickStats2)
+ .build();
+
+ // Check that searchIntentStats0 wasn't altered.
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats0.getPrevQuery()).isEqualTo("query1");
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("query2");
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1L);
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(2);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+ assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+ // Check that searchIntentStats1 has the new values.
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo("database2");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("query3");
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("query4");
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2L);
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(4);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats())
+ .containsExactly(clickStats0, clickStats1, clickStats2);
+ }
+
+ @Test
+ public void testBuilder_addClicksStats_byCollection() {
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+ Set<ClickStats> clicksStats = ImmutableSet.of(clickStats0, clickStats1);
+
+ final SearchIntentStats searchIntentStats =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addClicksStats(clicksStats)
+ .build();
+
+ assertThat(searchIntentStats.getClicksStats()).containsExactlyElementsIn(clicksStats);
+ }
+
+ @Test
+ public void testBuilder_builderReuse() {
+ String prevQuery = "prev";
+ String currQuery = "curr";
+ long searchIntentTimestampMillis = 1;
+ int numResultsFetched = 2;
+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+
+ SearchIntentStats.Builder builder =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery(prevQuery)
+ .setCurrQuery(currQuery)
+ .setTimestampMillis(searchIntentTimestampMillis)
+ .setNumResultsFetched(numResultsFetched)
+ .setQueryCorrectionType(queryCorrectionType)
+ .addClicksStats(clickStats0, clickStats1);
+
+ final SearchIntentStats searchIntentStats0 = builder.build();
+
+ final ClickStats clickStats2 =
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build();
+ builder.addClicksStats(clickStats2);
+
+ final SearchIntentStats searchIntentStats1 = builder.build();
+
+ // Check that searchIntentStats0 wasn't altered.
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+ // Check that searchIntentStats1 has the new values.
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats1.getClicksStats())
+ .containsExactly(clickStats0, clickStats1, clickStats2);
+ }
+
+ @Test
+ public void testBuilder_builderReuse_byCollection() {
+ String prevQuery = "prev";
+ String currQuery = "curr";
+ long searchIntentTimestampMillis = 1;
+ int numResultsFetched = 2;
+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+ final ClickStats clickStats0 =
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build();
+ final ClickStats clickStats1 =
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build();
+
+ SearchIntentStats.Builder builder =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery(prevQuery)
+ .setCurrQuery(currQuery)
+ .setTimestampMillis(searchIntentTimestampMillis)
+ .setNumResultsFetched(numResultsFetched)
+ .setQueryCorrectionType(queryCorrectionType)
+ .addClicksStats(ImmutableSet.of(clickStats0, clickStats1));
+
+ final SearchIntentStats searchIntentStats0 = builder.build();
+
+ final ClickStats clickStats2 =
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build();
+ builder.addClicksStats(ImmutableSet.of(clickStats2));
+
+ final SearchIntentStats searchIntentStats1 = builder.build();
+
+ // Check that searchIntentStats0 wasn't altered.
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+ // Check that searchIntentStats1 has the new values.
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+ assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+ assertThat(searchIntentStats1.getClicksStats())
+ .containsExactly(clickStats0, clickStats1, clickStats2);
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java
new file mode 100644
index 0000000..b287769
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2024 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchSessionStatsTest {
+ static final String TEST_PACKAGE_NAME = "package.test";
+ static final String TEST_DATA_BASE = "testDataBase";
+
+ @Test
+ public void testBuilder() {
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("")
+ .setCurrQuery("query1")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build(),
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+
+ final SearchSessionStats searchSessionStats =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+ .build();
+
+ assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchSessionStats.getSearchIntentsStats())
+ .containsExactly(searchIntentStats0, searchIntentStats1);
+ }
+
+ @Test
+ public void testBuilder_addSearchIntentsStats_byCollection() {
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("")
+ .setCurrQuery("query1")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build(),
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ Set<SearchIntentStats> searchIntentsStats =
+ ImmutableSet.of(searchIntentStats0, searchIntentStats1);
+
+ final SearchSessionStats searchSessionStats =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addSearchIntentsStats(searchIntentsStats)
+ .build();
+
+ assertThat(searchSessionStats.getSearchIntentsStats())
+ .containsExactlyElementsIn(searchIntentsStats);
+ }
+
+ @Test
+ public void testBuilder_builderReuse() {
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("")
+ .setCurrQuery("query1")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build(),
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+
+ SearchSessionStats.Builder builder =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addSearchIntentsStats(searchIntentStats0, searchIntentStats1);
+
+ final SearchSessionStats searchSessionStats0 = builder.build();
+
+ final SearchIntentStats searchIntentStats2 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query2")
+ .setCurrQuery("query3")
+ .setTimestampMillis(3L)
+ .setNumResultsFetched(6)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(13L)
+ .setTimeStayOnResultMillis(23L)
+ .setResultRankInBlock(33)
+ .setResultRankGlobal(43)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ builder.addSearchIntentsStats(searchIntentStats2);
+
+ final SearchSessionStats searchSessionStats1 = builder.build();
+
+ // Check that searchSessionStats0 wasn't altered.
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats())
+ .containsExactly(searchIntentStats0, searchIntentStats1);
+
+ // Check that searchSessionStats1 has the new values.
+ assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchSessionStats1.getSearchIntentsStats())
+ .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+ }
+
+ @Test
+ public void testBuilder_builderReuse_byCollection() {
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("")
+ .setCurrQuery("query1")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build(),
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+
+ SearchSessionStats.Builder builder =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addSearchIntentsStats(
+ ImmutableSet.of(searchIntentStats0, searchIntentStats1));
+
+ final SearchSessionStats searchSessionStats0 = builder.build();
+
+ final SearchIntentStats searchIntentStats2 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query2")
+ .setCurrQuery("query3")
+ .setTimestampMillis(3L)
+ .setNumResultsFetched(6)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(13L)
+ .setTimeStayOnResultMillis(23L)
+ .setResultRankInBlock(33)
+ .setResultRankGlobal(43)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ builder.addSearchIntentsStats(ImmutableSet.of(searchIntentStats2));
+
+ final SearchSessionStats searchSessionStats1 = builder.build();
+
+ // Check that searchSessionStats0 wasn't altered.
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats())
+ .containsExactly(searchIntentStats0, searchIntentStats1);
+
+ // Check that searchSessionStats1 has the new values.
+ assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+ assertThat(searchSessionStats1.getSearchIntentsStats())
+ .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+ }
+
+ @Test
+ public void testGetEndSessionSearchIntentStats() {
+ final SearchIntentStats searchIntentStats0 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("")
+ .setCurrQuery("query1")
+ .setTimestampMillis(1L)
+ .setNumResultsFetched(2)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(10L)
+ .setTimeStayOnResultMillis(20L)
+ .setResultRankInBlock(30)
+ .setResultRankGlobal(40)
+ .setIsGoodClick(false)
+ .build(),
+ new ClickStats.Builder()
+ .setTimestampMillis(11L)
+ .setTimeStayOnResultMillis(21L)
+ .setResultRankInBlock(31)
+ .setResultRankGlobal(41)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+ final SearchIntentStats searchIntentStats1 =
+ new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .setPrevQuery("query1")
+ .setCurrQuery("query2")
+ .setTimestampMillis(2L)
+ .setNumResultsFetched(4)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+ .addClicksStats(
+ new ClickStats.Builder()
+ .setTimestampMillis(12L)
+ .setTimeStayOnResultMillis(22L)
+ .setResultRankInBlock(32)
+ .setResultRankGlobal(42)
+ .setIsGoodClick(true)
+ .build())
+ .build();
+
+ final SearchSessionStats searchSessionStats =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+ .build();
+
+ SearchIntentStats endSessionSearchIntentStats =
+ searchSessionStats.getEndSessionSearchIntentStats();
+ // End session SearchIntentStats should be identical to the last added SearchIntentStats,
+ // except the previous query is null and query correction type is
+ // QUERY_CORRECTION_TYPE_END_SESSION.
+ assertThat(endSessionSearchIntentStats).isNotNull();
+ assertThat(endSessionSearchIntentStats.getPackageName())
+ .isEqualTo(searchIntentStats1.getPackageName());
+ assertThat(endSessionSearchIntentStats.getDatabase())
+ .isEqualTo(searchIntentStats1.getDatabase());
+ assertThat(endSessionSearchIntentStats.getCurrQuery())
+ .isEqualTo(searchIntentStats1.getCurrQuery());
+ assertThat(endSessionSearchIntentStats.getTimestampMillis())
+ .isEqualTo(searchIntentStats1.getTimestampMillis());
+ assertThat(endSessionSearchIntentStats.getNumResultsFetched())
+ .isEqualTo(searchIntentStats1.getNumResultsFetched());
+ assertThat(endSessionSearchIntentStats.getClicksStats())
+ .containsExactlyElementsIn(searchIntentStats1.getClicksStats());
+
+ assertThat(endSessionSearchIntentStats.getPrevQuery()).isNull();
+ assertThat(endSessionSearchIntentStats.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION);
+ }
+
+ @Test
+ public void testGetEndSessionSearchIntentStats_emptySearchIntentsShouldReturnNull() {
+ // Create a SearchSessionStats without search intents.
+ final SearchSessionStats searchSessionStats =
+ new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+ .setDatabase(TEST_DATA_BASE)
+ .build();
+
+ assertThat(searchSessionStats.getEndSessionSearchIntentStats()).isNull();
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java
new file mode 100644
index 0000000..36e0ca0
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+
+import org.junit.Test;
+
+public class ClickActionGenericDocumentTest {
+ @Test
+ public void testBuild() {
+ ClickActionGenericDocument clickActionGenericDocument =
+ new ClickActionGenericDocument.Builder("namespace", "click", "builtin:ClickAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("body")
+ .setResultRankInBlock(12)
+ .setResultRankGlobal(34)
+ .setTimeStayOnResultMillis(2000)
+ .build();
+
+ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(clickActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+ }
+
+ @Test
+ public void testBuild_fromGenericDocument() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+ .setCreationTimestampMillis(1000)
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+ .setPropertyString("query", "body")
+ .setPropertyLong("resultRankInBlock", 12)
+ .setPropertyLong("resultRankGlobal", 34)
+ .setPropertyLong("timeStayOnResultMillis", 2000)
+ .build();
+ ClickActionGenericDocument clickActionGenericDocument =
+ new ClickActionGenericDocument.Builder(document).build();
+
+ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(clickActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+ }
+
+// @exportToFramework:startStrip()
+ @Test
+ public void testBuild_fromDocumentClass() throws Exception {
+ ClickAction clickAction =
+ new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */1000)
+ .setQuery("body")
+ .setReferencedQualifiedId("pkg$db/ns#doc")
+ .setResultRankInBlock(12)
+ .setResultRankGlobal(34)
+ .setTimeStayOnResultMillis(2000)
+ .build();
+ ClickActionGenericDocument clickActionGenericDocument =
+ new ClickActionGenericDocument.Builder(
+ GenericDocument.fromDocumentClass(clickAction)).build();
+
+ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(clickActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+ }
+// @exportToFramework:endStrip()
+
+ @Test
+ public void testBuild_invalidActionTypeThrowsException() {
+ GenericDocument documentWithoutActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")
+ .build();
+ IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,
+ () -> new ClickActionGenericDocument.Builder(documentWithoutActionType));
+ assertThat(e1.getMessage())
+ .isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+ GenericDocument documentWithUnknownActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+ .build();
+ IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,
+ () -> new ClickActionGenericDocument.Builder(documentWithUnknownActionType));
+ assertThat(e2.getMessage())
+ .isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+ GenericDocument documentWithIncorrectActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+ .build();
+ IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,
+ () -> new ClickActionGenericDocument.Builder(documentWithIncorrectActionType));
+ assertThat(e3.getMessage())
+ .isEqualTo("Invalid action type for ClickActionGenericDocument");
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
new file mode 100644
index 0000000..ea705b7
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+
+import org.junit.Test;
+
+public class SearchActionGenericDocumentTest {
+ @Test
+ public void testBuild() {
+ SearchActionGenericDocument searchActionGenericDocument =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("body")
+ .setFetchedResultCount(123)
+ .build();
+
+ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(searchActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+ }
+
+ @Test
+ public void testBuild_fromGenericDocument() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+ .setPropertyString("query", "body")
+ .setPropertyLong("fetchedResultCount", 123)
+ .build();
+ SearchActionGenericDocument searchActionGenericDocument =
+ new SearchActionGenericDocument(document);
+
+ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(searchActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+ }
+
+// @exportToFramework:startStrip()
+ @Test
+ public void testBuild_fromDocumentClass() throws Exception {
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+ .setQuery("body")
+ .setFetchedResultCount(123)
+ .build();
+ SearchActionGenericDocument searchActionGenericDocument =
+ new SearchActionGenericDocument(GenericDocument.fromDocumentClass(searchAction));
+
+ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+ assertThat(searchActionGenericDocument.getActionType())
+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+ }
+// @exportToFramework:endStrip()
+
+ @Test
+ public void testBuild_invalidActionTypeThrowsException() {
+ GenericDocument documentWithoutActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .build();
+ IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,
+ () -> new SearchActionGenericDocument.Builder(documentWithoutActionType));
+ assertThat(e1.getMessage())
+ .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+ GenericDocument documentWithUnknownActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+ .build();
+ IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,
+ () -> new SearchActionGenericDocument.Builder(documentWithUnknownActionType));
+ assertThat(e2.getMessage())
+ .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+ GenericDocument documentWithIncorrectActionType =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+ .build();
+ IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,
+ () -> new SearchActionGenericDocument.Builder(documentWithIncorrectActionType));
+ assertThat(e3.getMessage())
+ .isEqualTo("Invalid action type for SearchActionGenericDocument");
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
new file mode 100644
index 0000000..24b59a6
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
@@ -0,0 +1,1349 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchSessionStatsExtractorTest {
+ private static final String TEST_PACKAGE_NAME = "test.package.name";
+ private static final String TEST_DATABASE = "database";
+
+ @Test
+ public void testExtract() {
+ // Create search action and click action generic documents.
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("tes")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("tes")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(512)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+ .build();
+ GenericDocument clickAction2 =
+ new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(3000)
+ .setQuery("tes")
+ .setResultRankInBlock(3)
+ .setResultRankGlobal(6)
+ .setTimeStayOnResultMillis(1024)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(5000)
+ .setQuery("test")
+ .setFetchedResultCount(10)
+ .build();
+ GenericDocument clickAction3 =
+ new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+ .setCreationTimestampMillis(6000)
+ .setQuery("test")
+ .setResultRankInBlock(2)
+ .setResultRankGlobal(4)
+ .setTimeStayOnResultMillis(512)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc3")
+ .build();
+ GenericDocument clickAction4 =
+ new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+ .setCreationTimestampMillis(7000)
+ .setQuery("test")
+ .setResultRankInBlock(4)
+ .setResultRankGlobal(8)
+ .setTimeStayOnResultMillis(256)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc4")
+ .build();
+ GenericDocument clickAction5 =
+ new ClickActionGenericDocument.Builder("namespace", "click5", "builtin:ClickAction")
+ .setCreationTimestampMillis(8000)
+ .setQuery("test")
+ .setResultRankInBlock(6)
+ .setResultRankGlobal(12)
+ .setTimeStayOnResultMillis(2048)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc5")
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(
+ searchAction1,
+ clickAction1,
+ clickAction2,
+ searchAction2,
+ clickAction3,
+ clickAction4,
+ clickAction5);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).hasSize(2);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(512);
+ assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+ assertThat(searchIntentStats0.getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(1024);
+ assertThat(searchIntentStats0.getClicksStats().get(1).isGoodClick()).isFalse();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(5000);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("tes");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).hasSize(3);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(512);
+ assertThat(searchIntentStats1.getClicksStats().get(0).isGoodClick()).isFalse();
+ assertThat(searchIntentStats1.getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(256);
+ assertThat(searchIntentStats1.getClicksStats().get(1).isGoodClick()).isFalse();
+ assertThat(searchIntentStats1.getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getTimeStayOnResultMillis())
+ .isEqualTo(2048);
+ assertThat(searchIntentStats1.getClicksStats().get(2).isGoodClick()).isTrue();
+ }
+
+ @Test
+ public void testExtract_noSearchActionShouldReturnEmptyList() {
+ // Create search action and click action generic documents.
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("tes")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(512)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+ .build();
+ GenericDocument clickAction2 =
+ new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(3000)
+ .setQuery("tes")
+ .setResultRankInBlock(3)
+ .setResultRankGlobal(6)
+ .setTimeStayOnResultMillis(1024)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(clickAction1, clickAction2);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void testExtract_shouldSkipUnknownActionTypeDocuments() {
+ // Create search action and click action generic documents.
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("tes")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument clickAction1 =
+ new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setPropertyString("query", "tes")
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+ .setPropertyLong("resultRankInBlock", 1)
+ .setPropertyLong("resultRankGlobal", 2)
+ .setPropertyLong("timeStayOnResultMillis", 512)
+ .build();
+ GenericDocument clickAction2 =
+ new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(3000)
+ .setQuery("tes")
+ .setResultRankInBlock(3)
+ .setResultRankGlobal(6)
+ .setTimeStayOnResultMillis(1024)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+
+ // Since clickAction1 doesn't have property "actionType", it should be skipped without
+ // throwing any exception.
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).hasSize(1);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(3000);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(3);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(6);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(1024);
+ assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+ }
+
+// @exportToFramework:startStrip()
+ @Test
+ public void testExtract_builtFromDocumentClass() throws Exception {
+ SearchAction searchAction1 =
+ new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+ .setQuery("tes")
+ .setFetchedResultCount(20)
+ .build();
+ ClickAction clickAction1 =
+ new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+ .setQuery("tes")
+ .setReferencedQualifiedId("pkg$db/ns#doc1")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(512)
+ .build();
+ ClickAction clickAction2 =
+ new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+ .setQuery("tes")
+ .setReferencedQualifiedId("pkg$db/ns#doc2")
+ .setResultRankInBlock(3)
+ .setResultRankGlobal(6)
+ .setTimeStayOnResultMillis(1024)
+ .build();
+ SearchAction searchAction2 =
+ new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */5000)
+ .setQuery("test")
+ .setFetchedResultCount(10)
+ .build();
+ ClickAction clickAction3 =
+ new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */6000)
+ .setQuery("test")
+ .setReferencedQualifiedId("pkg$db/ns#doc3")
+ .setResultRankInBlock(2)
+ .setResultRankGlobal(4)
+ .setTimeStayOnResultMillis(512)
+ .build();
+ ClickAction clickAction4 =
+ new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */7000)
+ .setQuery("test")
+ .setReferencedQualifiedId("pkg$db/ns#doc4")
+ .setResultRankInBlock(4)
+ .setResultRankGlobal(8)
+ .setTimeStayOnResultMillis(256)
+ .build();
+ ClickAction clickAction5 =
+ new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */8000)
+ .setQuery("test")
+ .setReferencedQualifiedId("pkg$db/ns#doc5")
+ .setResultRankInBlock(6)
+ .setResultRankGlobal(12)
+ .setTimeStayOnResultMillis(2048)
+ .build();
+
+ // Use PutDocumentsRequest taken action API to convert document class to GenericDocument.
+ PutDocumentsRequest putDocumentsRequest = new PutDocumentsRequest.Builder()
+ .addTakenActions(searchAction1, clickAction1, clickAction2,
+ searchAction2, clickAction3, clickAction4, clickAction5)
+ .build();
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE,
+ putDocumentsRequest.getTakenActionGenericDocuments());
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).hasSize(2);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+ assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(512);
+ assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+ assertThat(searchIntentStats0.getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+ assertThat(searchIntentStats0.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(1024);
+ assertThat(searchIntentStats0.getClicksStats().get(1).isGoodClick()).isFalse();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(5000);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("tes");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).hasSize(3);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+ assertThat(searchIntentStats1.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(512);
+ assertThat(searchIntentStats1.getClicksStats().get(0).isGoodClick()).isFalse();
+ assertThat(searchIntentStats1.getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+ assertThat(searchIntentStats1.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(256);
+ assertThat(searchIntentStats1.getClicksStats().get(1).isGoodClick()).isFalse();
+ assertThat(searchIntentStats1.getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+ assertThat(searchIntentStats1.getClicksStats().get(2).getTimeStayOnResultMillis())
+ .isEqualTo(2048);
+ assertThat(searchIntentStats1.getClicksStats().get(2).isGoodClick()).isTrue();
+ }
+// @exportToFramework:endStrip()
+
+ @Test
+ public void testExtract_detectAndSkipSearchNoise_appendNewCharacters() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("te")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(3000)
+ .setQuery("tes")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction4 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search4", "builtin:SearchAction")
+ .setCreationTimestampMillis(3001)
+ .setQuery("test")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction5 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search5", "builtin:SearchAction")
+ .setCreationTimestampMillis(10000)
+ .setQuery("testing")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(
+ searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+ // searchAction2, searchAction3 should be considered as noise since they're intermediate
+ // search actions with no clicks. The extractor should create search intents only for the
+ // others.
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 2
+ SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("testing");
+ assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("test");
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_detectAndSkipSearchNoise_deleteCharacters() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("testing")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("test")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(3000)
+ .setQuery("tes")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction4 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search4", "builtin:SearchAction")
+ .setCreationTimestampMillis(3001)
+ .setQuery("te")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction5 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search5", "builtin:SearchAction")
+ .setCreationTimestampMillis(10000)
+ .setQuery("t")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(
+ searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+ // searchAction2, searchAction3 should be considered as noise since they're intermediate
+ // search actions with no clicks. The extractor should create search intents only for the
+ // others.
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("testing");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("testing");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 2
+ SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_occursAfterThresholdShouldNotBeSearchNoise() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(3001)
+ .setQuery("te")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(10000)
+ .setQuery("test")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+ // searchAction2 should not be considered as noise since it occurs after the threshold from
+ // searchAction1 (and therefore not intermediate search actions).
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 2
+ SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_nonPrefixQueryStringShouldNotBeSearchNoise() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("apple")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(1500)
+ .setQuery("application")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("email")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction4 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search4", "builtin:SearchAction")
+ .setCreationTimestampMillis(10000)
+ .setQuery("google")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2, searchAction3, searchAction4);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(4);
+
+ // searchAction2 and searchAction3 should not be considered as noise since neither query
+ // string is a prefix of the previous one (and therefore not intermediate search actions).
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("apple");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(1500);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("application");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("apple");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 2
+ SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("email");
+ assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("application");
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 3
+ SearchIntentStats searchIntentStats3 = searchSessionStats0.getSearchIntentsStats().get(3);
+ assertThat(searchIntentStats3.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats3.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats3.getTimestampMillis()).isEqualTo(10000);
+ assertThat(searchIntentStats3.getCurrQuery()).isEqualTo("google");
+ assertThat(searchIntentStats3.getPrevQuery()).isEqualTo("email");
+ assertThat(searchIntentStats3.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats3.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+ assertThat(searchIntentStats3.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_lastSearchActionShouldNotBeSearchNoise() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("te")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+ // searchAction2 should not be considered as noise since it is the last search action (and
+ // therefore not an intermediate search action).
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_lastSearchActionOfRelatedSearchSequenceShouldNotBeSearchNoise() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("te")
+ .setFetchedResultCount(0)
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(602001)
+ .setQuery("test")
+ .setFetchedResultCount(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ // searchAction2 should not be considered as noise:
+ // - searchAction3 is independent from searchAction2 and therefore forms an independent
+ // search session.
+ // - So searchAction2 is the last search action of its search session (and therefore not an
+ // intermediate search action).
+ assertThat(result).hasSize(2);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+ SearchSessionStats searchSessionStats1 = result.get(1);
+ assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+ // Search session 1, search intent 0
+ SearchIntentStats searchIntentStats2 = searchSessionStats1.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(602001);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats2.getPrevQuery()).isNull();
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_withClickActionShouldNotBeSearchNoise() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(2000)
+ .setQuery("te")
+ .setFetchedResultCount(10)
+ .build();
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2050)
+ .setQuery("te")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(512)
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+ .build();
+ GenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setCreationTimestampMillis(10000)
+ .setQuery("test")
+ .setFetchedResultCount(5)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2, clickAction1, searchAction3);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+ // Even though searchAction2 is an intermediate search action, it should not be considered
+ // as noise since there is at least 1 valid click action associated with it.
+
+ // Search session 0, search intent 0
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 0, search intent 1
+ SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats1.getClicksStats()).hasSize(1);
+
+ // Search session 0, search intent 2
+ SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+ assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+ assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+ assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+ assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(5);
+ assertThat(searchIntentStats2.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_independentSearchIntentShouldStartNewSearchSession() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setCreationTimestampMillis(601001)
+ .setQuery("te")
+ .setFetchedResultCount(10)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, searchAction2);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ // Since time difference between searchAction1 and searchAction2 exceeds the threshold,
+ // searchAction2 should be considered as an independent search intent and therefore a new
+ // search session stats is created.
+ assertThat(result).hasSize(2);
+
+ // Search session 0
+ SearchSessionStats searchSessionStats0 = result.get(0);
+ assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+ SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+ assertThat(searchIntentStats0.getPrevQuery()).isNull();
+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+ assertThat(searchIntentStats0.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+ // Search session 1
+ SearchSessionStats searchSessionStats1 = result.get(1);
+ assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+ SearchIntentStats searchIntentStats1 = searchSessionStats1.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(601001);
+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+ assertThat(searchIntentStats1.getPrevQuery()).isNull();
+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+ assertThat(searchIntentStats1.getQueryCorrectionType())
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+ }
+
+ @Test
+ public void testExtract_shouldSetIsGoodClick() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setTimeStayOnResultMillis(2001)
+ .build();
+ GenericDocument clickAction2 =
+ new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(4500)
+ .setTimeStayOnResultMillis(1999)
+ .build();
+ GenericDocument clickAction3 =
+ new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+ .setCreationTimestampMillis(7000)
+ .setTimeStayOnResultMillis(1)
+ .build();
+ GenericDocument clickAction4 =
+ new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+ .setCreationTimestampMillis(7500)
+ .setTimeStayOnResultMillis(2000)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(
+ searchAction1, clickAction1, clickAction2, clickAction3, clickAction4);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats = result.get(0);
+ assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+ SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats.getClicksStats()).hasSize(4);
+
+ assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(2001);
+ assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+ assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(1999);
+ assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isFalse();
+
+ assertThat(searchIntentStats.getClicksStats().get(2).getTimeStayOnResultMillis())
+ .isEqualTo(1);
+ assertThat(searchIntentStats.getClicksStats().get(2).isGoodClick()).isFalse();
+
+ assertThat(searchIntentStats.getClicksStats().get(3).getTimeStayOnResultMillis())
+ .isEqualTo(2000);
+ assertThat(searchIntentStats.getClicksStats().get(3).isGoodClick()).isTrue();
+ }
+
+ @Test
+ public void testExtract_unsetTimeStayOnResultShouldBeGoodClick() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, clickAction1);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats = result.get(0);
+ assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+ SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+ assertThat(result).hasSize(1);
+ assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+ assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(0);
+ assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+ }
+
+ @Test
+ public void testExtract_nonPositiveTimeStayOnResultShouldBeGoodClick() {
+ GenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setQuery("t")
+ .setFetchedResultCount(20)
+ .build();
+ GenericDocument clickAction1 =
+ new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setTimeStayOnResultMillis(-1)
+ .build();
+ GenericDocument clickAction2 =
+ new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(3000)
+ .setTimeStayOnResultMillis(0)
+ .build();
+
+ List<GenericDocument> takenActionGenericDocuments =
+ Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+ List<SearchSessionStats> result =
+ new SearchSessionStatsExtractor()
+ .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+ assertThat(result).hasSize(1);
+
+ SearchSessionStats searchSessionStats = result.get(0);
+ assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+ assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+ SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+ assertThat(searchIntentStats.getClicksStats()).hasSize(2);
+
+ assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+ .isEqualTo(-1);
+ assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+ assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+ .isEqualTo(0);
+ assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isTrue();
+ }
+
+ @Test
+ public void testGetQueryCorrectionType_unknown() {
+ SearchActionGenericDocument searchAction =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setQuery("test")
+ .build();
+ SearchActionGenericDocument searchActionWithNullQueryStr =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .build();
+
+ // Query correction type should be unknown if the current search action's query string is
+ // null.
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchActionWithNullQueryStr,
+ /* prevSearchAction= */ null))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchActionWithNullQueryStr,
+ /* prevSearchAction= */ searchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+
+ // Query correction type should be unknown if the previous search action contains null query
+ // string.
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction,
+ /* prevSearchAction= */ searchActionWithNullQueryStr))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchActionWithNullQueryStr,
+ /* prevSearchAction= */ searchActionWithNullQueryStr))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+ }
+
+ @Test
+ public void testGetQueryCorrectionType_firstQuery() {
+ SearchActionGenericDocument currSearchAction =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setQuery("test")
+ .build();
+
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ currSearchAction, /* prevSearchAction= */ null))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+ }
+
+ @Test
+ public void testGetQueryCorrectionType_refinement() {
+ SearchActionGenericDocument prevSearchAction =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "baseSearch", "builtin:SearchAction")
+ .setQuery("test")
+ .build();
+
+ // Append 1 new character should be query refinement.
+ SearchActionGenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setQuery("teste")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction1, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+ // Append 2 new characters should be query refinement.
+ SearchActionGenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setQuery("tester")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction2, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+ // Backspace 1 character should be query refinement.
+ SearchActionGenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setQuery("tes")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction3, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+ // Backspace 1 character and append new character(s) should be query refinement.
+ SearchActionGenericDocument searchAction4 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search4", "builtin:SearchAction")
+ .setQuery("tesla")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction4, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+ }
+
+ @Test
+ public void testGetQueryCorrectionType_abandonment() {
+ SearchActionGenericDocument prevSearchAction =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "baseSearch", "builtin:SearchAction")
+ .setQuery("test")
+ .build();
+
+ // Completely different query should be query abandonment.
+ SearchActionGenericDocument searchAction1 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search1", "builtin:SearchAction")
+ .setQuery("unit")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction1, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+ // Backspace 2 characters should be query abandonment.
+ SearchActionGenericDocument searchAction2 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search2", "builtin:SearchAction")
+ .setQuery("te")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction2, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+ // Backspace 2 characters and append new character(s) should be query abandonment.
+ SearchActionGenericDocument searchAction3 =
+ new SearchActionGenericDocument.Builder(
+ "namespace", "search3", "builtin:SearchAction")
+ .setQuery("texas")
+ .build();
+ assertThat(
+ SearchSessionStatsExtractor.getQueryCorrectionType(
+ /* currSearchAction= */ searchAction3, prevSearchAction))
+ .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
new file mode 100644
index 0000000..e50274b
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 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 static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+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;
+
+// "V2 schema" refers to V2 of the VisibilityDocument schema, but no Visibility overlay schema
+// present. Simulates backwards compatibility situations.
+public class VisibilityStoreMigrationFromV2Test {
+
+ /**
+ * 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_from2() throws Exception {
+ // As such, we can treat V2 documents as V3 documents when upgrading, but we need to test
+ // this.
+
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[32];
+ PackageIdentifier packageIdentifierFoo =
+ new PackageIdentifier(packageNameFoo, sha256CertFoo);
+
+ // Values for a "bar" client
+ String packageNameBar = "packageBar";
+ byte[] sha256CertBar = new byte[32];
+ PackageIdentifier packageIdentifierBar =
+ new PackageIdentifier(packageNameBar, sha256CertBar);
+
+ // Create AppSearchImpl with visibility document version 2;
+ AppSearchImpl appSearchImplInV2 = AppSearchImpl.create(mFile,
+ new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
+
+ // Erase overlay schemas since it doesn't exist in released V2 schema.
+ InternalSetSchemaResponse internalSetAndroidVSchemaResponse = appSearchImplInV2.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ // no overlay schema
+ ImmutableList.of(),
+ /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+ /*forceOverride=*/ true, // force push the old version into disk
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(internalSetAndroidVSchemaResponse.isSuccess()).isTrue();
+
+ GetSchemaResponse getSchemaResponse = appSearchImplInV2.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA);
+ GetSchemaResponse getAndroidVOverlaySchemaResponse = appSearchImplInV2.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+ assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).isEmpty();
+
+ // Build deprecated visibility documents in version 2
+ String prefix = PrefixUtil.createPrefix("package", "database");
+ InternalVisibilityConfig visibilityConfigV2 = new InternalVisibilityConfig.Builder(
+ prefix + "Schema")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(
+ new PackageIdentifier(packageNameFoo, sha256CertFoo))
+ .addVisibleToPackage(
+ new PackageIdentifier(packageNameBar, sha256CertBar))
+ .addVisibleToPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR))
+ .addVisibleToPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))
+ .addVisibleToPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ GenericDocument visibilityDocumentV2 =
+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfigV2);
+
+ // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to
+ // directly put old version of VisibilityDocument.
+ InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV2.setSchema(
+ "package",
+ "database",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Schema").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*schemaVersion=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+ // Put deprecated visibility documents in version 2 to AppSearchImpl
+ appSearchImplInV2.putDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ visibilityDocumentV2,
+ /*sendChangeNotifications=*/ false,
+ /*logger=*/null);
+
+ // Persist to disk and re-open the AppSearchImpl
+ appSearchImplInV2.close();
+ AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+ new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+ new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
+
+ InternalVisibilityConfig actualConfig =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Schema",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
+
+ assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+ assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+ .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+ assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+ .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)));
+
+ // Check that the visibility overlay schema was added.
+ getSchemaResponse = appSearchImpl.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA);
+ getAndroidVOverlaySchemaResponse = appSearchImpl.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+ assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+
+ // But no overlay document was created.
+ AppSearchException e = assertThrows(AppSearchException.class,
+ () -> appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ prefix + "Schema",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ assertThat(e).hasMessageThat().contains("not found");
+
+ appSearchImpl.close();
+ }
+}
+
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
index 7b03a10..c91f51c 100644
--- 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
@@ -29,8 +29,8 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.AppSearchImpl;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -82,19 +82,21 @@
// "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)
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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)
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
"package", "database"),
DEPRECATED_VISIBILITY_SCHEMA_TYPE)
@@ -131,34 +133,41 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
- VisibilityDocument actualDocument1 = new VisibilityDocument.Builder(
+ GenericDocument actualDocument1 =
appSearchImpl.getDocument(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Schema1",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
- VisibilityDocument actualDocument2 = new VisibilityDocument.Builder(
+ /*typePropertyPaths=*/ Collections.emptyMap());
+ GenericDocument actualDocument2 =
appSearchImpl.getDocument(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Schema2",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ /*typePropertyPaths=*/ Collections.emptyMap());
- VisibilityDocument expectedDocument1 =
- new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+ GenericDocument expectedDocument1 = VisibilityToDocumentConverter.createVisibilityDocument(
+ new InternalVisibilityConfig.Builder(prefix + "Schema1")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
- .build();
- VisibilityDocument expectedDocument2 =
- new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+ .build());
+ GenericDocument expectedDocument2 = VisibilityToDocumentConverter.createVisibilityDocument(
+ new InternalVisibilityConfig.Builder(prefix + "Schema2")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
- .build();
+ .build());
+
+ // Ignore the creation timestamp
+ actualDocument1 = new GenericDocument.Builder<>(actualDocument1)
+ .setCreationTimestampMillis(0).build();
+ actualDocument2 = new GenericDocument.Builder<>(actualDocument2)
+ .setCreationTimestampMillis(0).build();
+
assertThat(actualDocument1).isEqualTo(expectedDocument1);
assertThat(actualDocument2).isEqualTo(expectedDocument2);
appSearchImpl.close();
@@ -197,8 +206,8 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
InternalSetSchemaResponse internalSetSchemaResponse = appSearchImpl.setSchema(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
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
index a70da64..4b837ae 100644
--- 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
@@ -23,9 +23,9 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.AppSearchImpl;
import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -66,17 +66,21 @@
// Values for a "foo" client
String packageNameFoo = "packageFoo";
byte[] sha256CertFoo = new byte[32];
+ PackageIdentifier packageIdentifierFoo =
+ new PackageIdentifier(packageNameFoo, sha256CertFoo);
// Values for a "bar" client
String packageNameBar = "packageBar";
byte[] sha256CertBar = new byte[32];
+ PackageIdentifier packageIdentifierBar =
+ new PackageIdentifier(packageNameBar, sha256CertBar);
// Create AppSearchImpl with visibility document version 1;
AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile,
new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV1.setSchema(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
@@ -125,24 +129,24 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
new AppSearchConfigImpl(new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
- VisibilityDocument actualDocument = new VisibilityDocument.Builder(
- appSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefix + "Schema",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ InternalVisibilityConfig actualConfig =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ appSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ prefix + "Schema",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
- 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(
+ assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+ assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+ .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+ assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+ .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
index 056acea..5ac5bda 100644
--- 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
@@ -21,9 +21,13 @@
import static org.junit.Assert.assertThrows;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchConfigImpl;
import androidx.appsearch.localstorage.AppSearchImpl;
@@ -52,22 +56,21 @@
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();
+ File appSearchDir = mTemporaryFolder.newFolder();
mAppSearchImpl = AppSearchImpl.create(
- mAppSearchDir,
+ appSearchDir,
new AppSearchConfigImpl(
new UnlimitedLimitConfig(),
new LocalStorageIcingOptionsConfig()
),
/*initStatsBuilder=*/ null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ ALWAYS_OPTIMIZE);
mVisibilityStore = new VisibilityStore(mAppSearchImpl);
}
@@ -99,55 +102,83 @@
}
@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);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument =
- new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
+ public void testSetVisibilitySchema() throws Exception {
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA);
+
+
+ GetSchemaResponse getAndroidVOverlaySchemaResponse = mAppSearchImpl.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+ assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+ }
+
+ @Test
+ public void testSetAndGetVisibility() throws Exception {
+ String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+ InternalVisibilityConfig visibilityConfig =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+ assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+ .isEqualTo(visibilityConfig);
+ // Verify the VisibilityConfig is saved to AppSearchImpl.
+ GenericDocument actualDocument = mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ prefix + "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
- assertThat(actualDocument).isEqualTo(visibilityDocument);
+ /*typePropertyPaths=*/ Collections.emptyMap());
+ // Ignore the creation timestamp
+ actualDocument =
+ new GenericDocument.Builder<>(actualDocument).setCreationTimestampMillis(0).build();
+
+ assertThat(actualDocument).isEqualTo(
+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
}
@Test
public void testRemoveVisibility() throws Exception {
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
- mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
assertThat(mVisibilityStore.getVisibility("Email"))
- .isEqualTo(visibilityDocument);
- // Verify the VisibilityDocument is saved to AppSearchImpl.
- VisibilityDocument actualDocument = new VisibilityDocument.Builder(
- mAppSearchImpl.getDocument(
- VisibilityStore.VISIBILITY_PACKAGE_NAME,
- VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ "Email",
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
- assertThat(actualDocument).isEqualTo(visibilityDocument);
+ .isEqualTo(visibilityConfig);
+ // Verify the VisibilityConfig is saved to AppSearchImpl.
+ InternalVisibilityConfig actualConfig =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()),
+ /*androidVOverlayDocument=*/null);
+ assertThat(actualConfig).isEqualTo(visibilityConfig);
- mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityDocument.getId()));
+ mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityConfig.getSchemaType()));
assertThat(mVisibilityStore.getVisibility("Email")).isNull();
- // Verify the VisibilityDocument is removed from AppSearchImpl.
+ // Verify the VisibilityConfig is removed from AppSearchImpl.
AppSearchException e = assertThrows(AppSearchException.class,
() -> mAppSearchImpl.getDocument(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
/*id=*/ "Email",
/*typePropertyPaths=*/ Collections.emptyMap()));
assertThat(e).hasMessageThat().contains(
@@ -158,17 +189,17 @@
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();
+ AppSearchSchema brokenSchema = new AppSearchSchema.Builder(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA_TYPE).build();
// Index a broken schema into AppSearch, use the latest version to make it broken.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
Collections.singletonList(brokenSchema),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
- /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
/*setSchemaStatsBuilder=*/ null);
assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
// Create VisibilityStore should recover the broken schema
@@ -176,13 +207,270 @@
// We should be able to set and get Visibility settings.
String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
- VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder(
+ prefix + "Email")
.setNotDisplayedBySystem(true)
.addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
.build();
- mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
- .isEqualTo(visibilityDocument);
+ .isEqualTo(visibilityConfig);
+ }
+
+ @Test
+ public void testSetGetAndRemoveOverlayVisibility() throws Exception {
+ String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+ SchemaVisibilityConfig nestedvisibilityConfig = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+
+ InternalVisibilityConfig visibilityConfig =
+ new InternalVisibilityConfig.Builder(prefix + "Email")
+ .addVisibleToConfig(nestedvisibilityConfig)
+ .build();
+
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+ assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+ .isEqualTo(visibilityConfig);
+ // Verify the VisibilityConfig is saved to AppSearchImpl.
+ GenericDocument visibleToConfigOverlay = mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap());
+ // Ignore the creation timestamp
+ visibleToConfigOverlay = new GenericDocument.Builder<>(visibleToConfigOverlay)
+ .setCreationTimestampMillis(0).build();
+ assertThat(visibleToConfigOverlay).isEqualTo(VisibilityToDocumentConverter
+ .createAndroidVOverlay(visibilityConfig));
+
+ mVisibilityStore.removeVisibility(ImmutableSet.of(prefix + "Email"));
+ // Verify the VisibilityConfig is removed from AppSearchImpl.
+ AppSearchException e = assertThrows(AppSearchException.class,
+ () -> mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ prefix + "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ assertThat(e).hasMessageThat().contains("not found.");
+ }
+
+ @Test
+ public void testSetVisibility_avoidRemoveOverlay() throws Exception {
+ // Set a visibility config w/o overlay
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+ // Put a fake AndroidVOverlay into AppSearchImpl, this is not added by VisibilityStore,
+ // just add a fake AndroidVOverlay to verify we won't remove it when we update the config
+ // which doesn't contain any overlay settings.
+ GenericDocument fakeAndroidVOverlay =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+ "Email", "AndroidVOverlayType")
+ .setCreationTimestampMillis(0)
+ .build();
+ mAppSearchImpl.putDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ fakeAndroidVOverlay,
+ /*sendChangeNotifications=*/ false,
+ /*logger=*/null);
+
+ // update the visibility config w/o overlay
+ InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+ // Verify we won't trigger a remove() call to AppSearchImpl by get the fakeAndroidVOverlay.
+ GenericDocument actualAndroidVOverlay = mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap());
+
+ // Ignore the creation timestamp
+ actualAndroidVOverlay = new GenericDocument.Builder<>(actualAndroidVOverlay)
+ .setCreationTimestampMillis(0).build();
+ assertThat(actualAndroidVOverlay).isEqualTo(fakeAndroidVOverlay);
+ }
+
+ @Test
+ public void testSetVisibility_removeOverlay_publicAcl() throws Exception {
+ // Set a visibility config with public overlay
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .setPubliclyVisibleTargetPackage(
+ new PackageIdentifier("pkgBar", new byte[32]))
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+ // verify the overlay document is created.
+ mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap());
+
+ // update the visibility config w/o overlay
+ InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+ // Verify the overlay document is removed.
+ AppSearchException e = assertThrows(AppSearchException.class,
+ () -> mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ assertThat(e).hasMessageThat().contains("not found.");
+ }
+
+ @Test
+ public void testSetVisibility_removeOverlay_visibleToConfig() throws Exception {
+ // Set a visibility config with visible to config.
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .addVisibleToConfig(new SchemaVisibilityConfig.Builder()
+ .addRequiredPermissions(ImmutableSet.of(1)).build())
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+ // verify the overlay document is created.
+ mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap());
+
+ // update the visibility config w/o overlay
+ InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+ .setNotDisplayedBySystem(true)
+ .build();
+ mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+ // Verify the overlay document is removed.
+ AppSearchException e = assertThrows(AppSearchException.class,
+ () -> mAppSearchImpl.getDocument(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ "Email",
+ /*typePropertyPaths=*/ Collections.emptyMap()));
+ assertThat(e).hasMessageThat().contains("not found.");
+ }
+
+ @Test
+ public void testMigrateFromDeprecatedSchema() throws Exception {
+ // Set deprecated public acl schema to main visibility database.
+ mAppSearchImpl.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ ImmutableList.of(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA,
+ VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Create VisibilityStore with success and force remove deprecated public acl schema from
+ // the main visibility database.
+ mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.VISIBILITY_DATABASE_NAME,
+ new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA);
+ }
+
+ @Test
+ public void testMigrateFromDeprecatedOverlaySchema() throws Exception {
+ // Set deprecated overlay schema to overlay database.
+ AppSearchSchema deprecatedOverlaySchema =
+ new AppSearchSchema.Builder("AndroidVOverlayType")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ "publiclyVisibleTargetPackage")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ "publiclyVisibleTargetPackageSha256Cert")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "visibleToConfigProperty",
+ "VisibleToConfigType")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+ AppSearchSchema deprecatedVisibleToConfigSchema =
+ new AppSearchSchema.Builder("VisibleToConfigType")
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "notPlatformSurfaceable")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ "packageName")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ "sha256Cert")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "permission", VisibilityPermissionConfig.SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ "publiclyVisibleTargetPackage")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ "publiclyVisibleTargetPackageSha256Cert")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+ mAppSearchImpl.setSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ ImmutableList.of(deprecatedOverlaySchema, deprecatedVisibleToConfigSchema,
+ VisibilityPermissionConfig.SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityToDocumentConverter
+ .OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Create VisibilityStore with success and force remove override overlay schema.
+ mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+ VisibilityStore.VISIBILITY_PACKAGE_NAME,
+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+ new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+ assertThat(getSchemaResponse.getVersion()).isEqualTo(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST);
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
}
}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
new file mode 100644
index 0000000..7c50c0d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2024 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.GenericDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.SetSchemaRequest;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class VisibilityToDocumentConverterTest {
+
+ @Test
+ public void testToGenericDocuments() throws Exception {
+ // Create a SetSchemaRequest for testing
+ byte[] cert1 = new byte[32];
+ byte[] cert2 = new byte[32];
+ byte[] cert3 = new byte[32];
+ byte[] cert4 = new byte[32];
+ Arrays.fill(cert1, (byte) 1);
+ Arrays.fill(cert2, (byte) 2);
+ Arrays.fill(cert3, (byte) 3);
+ Arrays.fill(cert4, (byte) 4);
+
+ SchemaVisibilityConfig visibleToConfig = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+ .setPubliclyVisibleTargetPackage(
+ new PackageIdentifier("com.example.test2", cert2))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+ .setSchemaTypeDisplayedBySystem("someSchema", false)
+ .setSchemaTypeVisibilityForPackage("someSchema", true,
+ new PackageIdentifier("com.example.test3", cert3))
+ .addRequiredPermissionsForSchemaTypeVisibility(
+ "someSchema", ImmutableSet.of(3, 4))
+ .setPubliclyVisibleSchema("someSchema",
+ new PackageIdentifier("com.example.test4", cert4))
+ .addSchemaTypeVisibleToConfig("someSchema", visibleToConfig)
+ .build();
+
+ // Create android V overlay proto
+ VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+ .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test1")
+ .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test2")
+ .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+ .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+ .addAllPermissions(ImmutableSet.of(1, 2)).build())
+ .build();
+ VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test4")
+ .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+ .build();
+ AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+ .setVisibilityConfig(visibilityConfigProto)
+ .addVisibleToConfigs(visibleToConfigProto)
+ .build();
+
+ // Create the expected AndroidVOverlay document
+ GenericDocument expectedAndroidVOverlay =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+ "someSchema", "AndroidVOverlayType")
+ .setCreationTimestampMillis(0)
+ .setPropertyBytes("visibilityProtoSerializeProperty",
+ overlayProto.toByteArray())
+ .build();
+
+ // Create the expected visibility document
+ GenericDocument permissionDoc34 =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("", "",
+ "VisibilityPermissionType")
+ .setCreationTimestampMillis(0)
+ .setPropertyLong("allRequiredPermissions", 3, 4).build();
+ GenericDocument expectedVisibilityDocument =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("", "someSchema",
+ "VisibilityType")
+ .setCreationTimestampMillis(0)
+ .setPropertyBoolean("notPlatformSurfaceable", true)
+ .setPropertyString("packageName", "com.example.test3")
+ .setPropertyBytes("sha256Cert", cert3)
+ .setPropertyDocument("permission", permissionDoc34)
+ .build();
+
+ // Convert the SetSchemaRequest to a list of VisibilityConfig
+ List<InternalVisibilityConfig> visibilityConfigs =
+ InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+ // Check if the conversion is correct
+ assertThat(visibilityConfigs).hasSize(1);
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+ assertThat(expectedVisibilityDocument).isEqualTo(
+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+ assertThat(expectedAndroidVOverlay).isEqualTo(
+ VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig));
+ }
+
+ @Test
+ public void testToVisibilityConfig() throws Exception {
+ byte[] cert1 = new byte[32];
+ byte[] cert2 = new byte[32];
+ byte[] cert3 = new byte[32];
+ byte[] cert4 = new byte[32];
+ Arrays.fill(cert1, (byte) 1);
+ Arrays.fill(cert2, (byte) 2);
+ Arrays.fill(cert3, (byte) 3);
+ Arrays.fill(cert4, (byte) 4);
+
+ // Create visibility proto property
+ VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+ .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test1")
+ .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test2")
+ .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+ .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+ .addAllPermissions(ImmutableSet.of(1, 2)).build())
+ .build();
+ VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+ .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+ .setPackageName("com.example.test4")
+ .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+ .build();
+ AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+ .setVisibilityConfig(visibilityConfigProto)
+ .addVisibleToConfigs(visibleToConfigProto)
+ .build();
+
+ // Create a visible config overlay for testing
+ GenericDocument androidVOverlay =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+ "someSchema", "AndroidVOverlayType")
+ .setCreationTimestampMillis(0)
+ .setPropertyBytes("visibilityProtoSerializeProperty",
+ overlayProto.toByteArray())
+ .build();
+
+ // Create a VisibilityDocument for testing
+ GenericDocument permissionDoc34 =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("", "",
+ "VisibilityPermissionType")
+ .setCreationTimestampMillis(0)
+ .setPropertyLong("allRequiredPermissions", 3, 4).build();
+ GenericDocument visibilityDoc =
+ new GenericDocument.Builder<GenericDocument.Builder<?>>("", "someSchema",
+ "VisibilityType")
+ .setCreationTimestampMillis(0)
+ .setPropertyBoolean("notPlatformSurfaceable", true)
+ .setPropertyString("packageName", "com.example.test3")
+ .setPropertyBytes("sha256Cert", cert3)
+ .setPropertyDocument("permission", permissionDoc34)
+ .build();
+
+ // Create a VisibilityConfig using the Builder
+ InternalVisibilityConfig visibilityConfig =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ visibilityDoc, androidVOverlay);
+
+ // Check if the properties are set correctly
+ assertThat(visibilityDoc).isEqualTo(
+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+ GenericDocument actualOverlayDoc =
+ VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+ AndroidVOverlayProto actualOverlayProto = AndroidVOverlayProto.parseFrom(
+ actualOverlayDoc.getPropertyBytes("visibilityProtoSerializeProperty"));
+ assertThat(actualOverlayProto).isEqualTo(overlayProto);
+ assertThat(androidVOverlay).isEqualTo(actualOverlayDoc);
+
+ // Verify rebuild from InternalVisibilityConfig remains the same.
+ InternalVisibilityConfig.Builder builder =
+ new InternalVisibilityConfig.Builder(visibilityConfig);
+ InternalVisibilityConfig rebuild = builder.build();
+ assertThat(visibilityConfig).isEqualTo(rebuild);
+
+ InternalVisibilityConfig modifiedConfig = builder
+ .setSchemaType("prefixedSchema")
+ .setNotDisplayedBySystem(false)
+ .addVisibleToPermissions(ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR))
+ .clearVisibleToPackages()
+ .addVisibleToPackage(
+ new PackageIdentifier("com.example.other", new byte[32]))
+ .setPubliclyVisibleTargetPackage(
+ new PackageIdentifier("com.example.other", new byte[32]))
+ .clearVisibleToConfig()
+ .build();
+ assertThat(modifiedConfig.getSchemaType()).isEqualTo("prefixedSchema");
+
+ // Check that the rebuild stayed the same
+ assertThat(rebuild.getSchemaType()).isEqualTo("someSchema");
+ assertThat(rebuild.isNotDisplayedBySystem()).isTrue();
+ assertThat(rebuild.getVisibilityConfig().getRequiredPermissions())
+ .containsExactly(ImmutableSet.of(3, 4));
+ assertThat(rebuild.getVisibilityConfig().getAllowedPackages())
+ .containsExactly(new PackageIdentifier("com.example.test3", cert3));
+ assertThat(
+ rebuild.getVisibilityConfig().getPubliclyVisibleTargetPackage()).isEqualTo(
+ new PackageIdentifier("com.example.test4", cert4));
+
+ SchemaVisibilityConfig expectedVisibleToConfig = new SchemaVisibilityConfig.Builder()
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("com.example.test2", cert2))
+ .build();
+ assertThat(rebuild.getVisibleToConfigs()).containsExactly(expectedVisibleToConfig);
+ }
+
+ @Test
+ public void testToGenericDocumentAndBack() {
+ // Create a SetSchemaRequest for testing
+ byte[] cert1 = new byte[32];
+ byte[] cert2 = new byte[32];
+ byte[] cert3 = new byte[32];
+ byte[] cert4 = new byte[32];
+ byte[] cert5 = new byte[32];
+ byte[] cert6 = new byte[32];
+ byte[] cert7 = new byte[32];
+ Arrays.fill(cert1, (byte) 1);
+ Arrays.fill(cert2, (byte) 2);
+ Arrays.fill(cert3, (byte) 3);
+ Arrays.fill(cert4, (byte) 4);
+ Arrays.fill(cert5, (byte) 5);
+ Arrays.fill(cert6, (byte) 6);
+ Arrays.fill(cert7, (byte) 7);
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+ .setPubliclyVisibleTargetPackage(
+ new PackageIdentifier("com.example.test2", cert2))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("com.example.test3", cert3))
+ .addRequiredPermissions(ImmutableSet.of(3, 4))
+ .build();
+ SchemaVisibilityConfig config3 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("com.example.test4", cert4))
+ .setPubliclyVisibleTargetPackage(
+ new PackageIdentifier("com.example.test5", cert5))
+ .build();
+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+ .setSchemaTypeDisplayedBySystem("someSchema", /*displayed=*/ true)
+ .setSchemaTypeVisibilityForPackage("someSchema", /*visible=*/ true,
+ new PackageIdentifier("com.example.test6", cert6))
+ .addRequiredPermissionsForSchemaTypeVisibility("someSchema",
+ ImmutableSet.of(1, 2))
+ .setPubliclyVisibleSchema("someSchema",
+ new PackageIdentifier("com.example.test7", cert7))
+ .addSchemaTypeVisibleToConfig("someSchema", config1)
+ .addSchemaTypeVisibleToConfig("someSchema", config2)
+ .addSchemaTypeVisibleToConfig("someSchema", config3)
+ .build();
+
+ // Convert the SetSchemaRequest to a list of VisibilityConfig
+ List<InternalVisibilityConfig> visibilityConfigs =
+ InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+ GenericDocument visibilityDoc =
+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig);
+ GenericDocument androidVOverlay =
+ VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+
+ InternalVisibilityConfig rebuild =
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ visibilityDoc, androidVOverlay);
+
+ assertThat(rebuild).isEqualTo(visibilityConfig);
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
new file mode 100644
index 0000000..9a98739
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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 org.junit.Test;
+
+public class VisibilityUtilTest {
+ @Test
+ public void testIsSchemaSearchableByCaller_selfAccessDefaultAllowed() {
+ CallerAccess callerAccess = new CallerAccess("package1");
+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+ /*targetPackageName=*/ "package1",
+ /*prefixedSchema=*/ "schema",
+ /*visibilityStore=*/ null,
+ /*visibilityChecker=*/ null)).isTrue();
+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+ /*targetPackageName=*/ "package2",
+ /*prefixedSchema=*/ "schema",
+ /*visibilityStore=*/ null,
+ /*visibilityChecker=*/ null)).isFalse();
+ }
+
+ @Test
+ public void testIsSchemaSearchableByCaller_selfAccessNotAllowed() {
+ CallerAccess callerAccess = new CallerAccess("package1") {
+ @Override
+ public boolean doesCallerHaveSelfAccess() {
+ return false;
+ }
+ };
+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+ /*targetPackageName=*/ "package1",
+ /*prefixedSchema=*/ "schema",
+ /*visibilityStore=*/ null,
+ /*visibilityChecker=*/ null)).isFalse();
+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+ /*targetPackageName=*/ "package2",
+ /*prefixedSchema=*/ "schema",
+ /*visibilityStore=*/ null,
+ /*visibilityChecker=*/ null)).isFalse();
+ }
+}
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 c7cccb8..5cecaa7 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
@@ -48,6 +48,12 @@
// fall through
case Features.LIST_FILTER_QUERY_LANGUAGE:
// fall through
+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+ // fall through
+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+ // fall through
+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+ // fall through
case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
// fall through
case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
@@ -60,15 +66,23 @@
// fall through
case Features.SEARCH_SUGGESTION:
// fall through
- case Features.SCHEMA_SET_DELETION_PROPAGATION:
- // fall through
case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
// fall through
case Features.SCHEMA_ADD_PARENT_TYPE:
// fall through
+ case Features.SCHEMA_SET_DESCRIPTION:
+ // fall through
case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
// fall through
case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+ // fall through
+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+ // fall through
+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
return true;
default:
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 4c9fbfe..d175aa6 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,7 +16,6 @@
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.app.InternalSetSchemaResponse.newFailedSetSchemaResponse;
import static androidx.appsearch.app.InternalSetSchemaResponse.newSuccessfulSetSchemaResponse;
@@ -27,7 +26,6 @@
import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
-import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
@@ -43,15 +41,16 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SearchSuggestionResult;
import androidx.appsearch.app.SearchSuggestionSpec;
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;
@@ -187,12 +186,8 @@
@VisibleForTesting
final IcingSearchEngine mIcingSearchEngineLocked;
- // This map contains schema types and SchemaTypeConfigProtos for all package-database
- // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
- // prefixed schema type to its respective SchemaTypeConfigProto.
@GuardedBy("mReadWriteLock")
- private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
- new ArrayMap<>();
+ private final SchemaCache mSchemaCacheLocked = new SchemaCache();
// This map contains namespaces for all package-database prefixes. All values in the map are
// prefixed with the package-database prefix.
@@ -259,7 +254,7 @@
* <p>Instead, logger instance needs to be passed to each individual method, like create, query
* and putDocument.
*
- * @param initStatsBuilder collects stats for initialization if provided.
+ * @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.
@@ -269,8 +264,8 @@
@NonNull File icingDir,
@NonNull AppSearchConfig config,
@Nullable InitializeStats.Builder initStatsBuilder,
- @NonNull OptimizeStrategy optimizeStrategy,
- @Nullable VisibilityChecker visibilityChecker)
+ @Nullable VisibilityChecker visibilityChecker,
+ @NonNull OptimizeStrategy optimizeStrategy)
throws AppSearchException {
return new AppSearchImpl(icingDir, config, initStatsBuilder, optimizeStrategy,
visibilityChecker);
@@ -378,9 +373,12 @@
for (int i = 0; i < schemaProtoTypesList.size(); i++) {
SchemaTypeConfigProto schema = schemaProtoTypesList.get(i);
String prefixedSchemaType = schema.getSchemaType();
- addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema);
+ mSchemaCacheLocked.addToSchemaMap(getPrefix(prefixedSchemaType), schema);
}
+ // Populate schema parent-to-children map
+ mSchemaCacheLocked.rebuildSchemaParentToChildrenMap();
+
// Populate namespace map
List<String> prefixedNamespaceList =
getAllNamespacesResultProto.getNamespacesList();
@@ -463,10 +461,10 @@
* @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
+ * @param visibilityConfigs {@link InternalVisibilityConfig}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}
+ * that don't has a {@link InternalVisibilityConfig}
* 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
@@ -490,7 +488,7 @@
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<AppSearchSchema> schemas,
- @NonNull List<VisibilityDocument> visibilityDocuments,
+ @NonNull List<InternalVisibilityConfig> visibilityConfigs,
boolean forceOverride,
int version,
@Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -508,7 +506,7 @@
packageName,
databaseName,
schemas,
- visibilityDocuments,
+ visibilityConfigs,
forceOverride,
version,
setSchemaStatsBuilder);
@@ -517,7 +515,7 @@
packageName,
databaseName,
schemas,
- visibilityDocuments,
+ visibilityConfigs,
forceOverride,
version,
setSchemaStatsBuilder);
@@ -539,7 +537,7 @@
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<AppSearchSchema> schemas,
- @NonNull List<VisibilityDocument> visibilityDocuments,
+ @NonNull List<InternalVisibilityConfig> visibilityConfigs,
boolean forceOverride,
int version,
@Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -588,7 +586,7 @@
packageName,
databaseName,
schemas,
- visibilityDocuments,
+ visibilityConfigs,
forceOverride,
version,
setSchemaStatsBuilder);
@@ -717,7 +715,7 @@
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<AppSearchSchema> schemas,
- @NonNull List<VisibilityDocument> visibilityDocuments,
+ @NonNull List<InternalVisibilityConfig> visibilityConfigs,
boolean forceOverride,
int version,
@Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -791,42 +789,47 @@
// Update derived data structures.
for (SchemaTypeConfigProto schemaTypeConfigProto :
rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
- addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+ mSchemaCacheLocked.addToSchemaMap(prefix, schemaTypeConfigProto);
}
for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
- removeFromMap(mSchemaMapLocked, prefix, schemaType);
+ mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType);
}
+
+ mSchemaCacheLocked.rebuildSchemaParentToChildrenMapForPrefix(prefix);
+
// 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
+ List<InternalVisibilityConfig> prefixedVisibilityConfigs =
+ new ArrayList<>(visibilityConfigs.size());
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+ // The VisibilityConfig 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.Builder(
- unPrefixedDocument).setId(prefixedSchemaType).build());
+ // TODO(b/275592563): Move prefixing into VisibilityConfig.createVisibilityDocument
+ // and createVisibilityOverlay
+ String schemaType = visibilityConfig.getSchemaType();
+ String prefixedSchemaType = prefix + schemaType;
+ prefixedVisibilityConfigs.add(new InternalVisibilityConfig.Builder(visibilityConfig)
+ .setSchemaType(prefixedSchemaType).build());
// This schema has visibility settings. We should keep it from the removal list.
- deprecatedVisibilityDocuments.remove(prefixedSchemaType);
+ deprecatedVisibilityDocuments.remove(visibilityConfig.getSchemaType());
}
// 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);
+ mVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs);
}
long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime();
if (setSchemaStatsBuilder != null) {
@@ -904,33 +907,44 @@
// schema. Avoid call visibility store before we have already created it.
if (mVisibilityStoreLocked != null) {
String typeName = typeConfig.getSchemaType().substring(typePrefix.length());
- VisibilityDocument visibilityDocument =
+ InternalVisibilityConfig visibilityConfig =
mVisibilityStoreLocked.getVisibility(prefixedSchemaType);
- if (visibilityDocument != null) {
- if (visibilityDocument.isNotDisplayedBySystem()) {
- responseBuilder
- .addSchemaTypeNotDisplayedBySystem(typeName);
+ if (visibilityConfig != null) {
+ if (visibilityConfig.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]));
- }
+ List<PackageIdentifier> packageIdentifiers =
+ visibilityConfig.getVisibilityConfig().getAllowedPackages();
+ if (!packageIdentifiers.isEmpty()) {
responseBuilder.setSchemaTypeVisibleToPackages(typeName,
- packageIdentifier);
+ new ArraySet<>(packageIdentifiers));
}
Set<Set<Integer>> visibleToPermissions =
- visibilityDocument.getVisibleToPermissions();
- if (visibleToPermissions != null) {
- responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(
- typeName, visibleToPermissions);
+ visibilityConfig.getVisibilityConfig().getRequiredPermissions();
+ if (!visibleToPermissions.isEmpty()) {
+ Set<Set<Integer>> visibleToPermissionsSet =
+ new ArraySet<>(visibleToPermissions.size());
+ for (Set<Integer> permissionList : visibleToPermissions) {
+ visibleToPermissionsSet.add(new ArraySet<>(permissionList));
+ }
+
+ responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(typeName,
+ visibleToPermissionsSet);
+ }
+
+ // Check for Visibility properties from the overlay
+ PackageIdentifier publiclyVisibleFromPackage =
+ visibilityConfig.getVisibilityConfig()
+ .getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleFromPackage != null) {
+ responseBuilder.setPubliclyVisibleSchema(
+ typeName, publiclyVisibleFromPackage);
+ }
+ Set<SchemaVisibilityConfig> visibleToConfigs =
+ visibilityConfig.getVisibleToConfigs();
+ if (!visibleToConfigs.isEmpty()) {
+ responseBuilder.setSchemaTypeVisibleToConfigs(
+ typeName, visibleToConfigs);
}
}
}
@@ -1190,7 +1204,7 @@
removePrefixesFromDocument(documentBuilder);
String prefix = createPrefix(packageName, databaseName);
Map<String, SchemaTypeConfigProto> schemaTypeMap =
- Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
+ mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
prefix, schemaTypeMap, mConfig);
} finally {
@@ -1232,7 +1246,7 @@
// 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 =
- Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
+ mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
prefix, schemaTypeMap, mConfig);
} finally {
@@ -1254,6 +1268,8 @@
*/
@NonNull
@GuardedBy("mReadWriteLock")
+ // We only log getResultProto.toString() in fullPii trace for debugging.
+ @SuppressWarnings("LiteProtoToString")
private DocumentProto getDocumentProtoByIdLocked(
@NonNull String packageName,
@NonNull String databaseName,
@@ -1317,8 +1333,9 @@
SearchStats.Builder sStatsBuilder = null;
if (logger != null) {
sStatsBuilder =
- new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL,
- packageName).setDatabase(databaseName);
+ new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName)
+ .setDatabase(databaseName)
+ .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag());
}
long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();
@@ -1338,18 +1355,18 @@
if (sStatsBuilder != null && logger != null) {
sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR);
}
- return new SearchResultPage(Bundle.EMPTY);
+ return new SearchResultPage();
}
String prefix = createPrefix(packageName, databaseName);
SearchSpecToProtoConverter searchSpecToProtoConverter =
new SearchSpecToProtoConverter(queryExpression, searchSpec,
- Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
+ Collections.singleton(prefix), mNamespaceMapLocked, mSchemaCacheLocked,
mConfig);
if (searchSpecToProtoConverter.hasNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return an
// empty SearchResult and skip sending request to Icing.
- return new SearchResultPage(Bundle.EMPTY);
+ return new SearchResultPage();
}
SearchResultPage searchResultPage =
@@ -1394,7 +1411,8 @@
sStatsBuilder =
new SearchStats.Builder(
SearchStats.VISIBILITY_SCOPE_GLOBAL,
- callerAccess.getCallingPackageName());
+ callerAccess.getCallingPackageName())
+ .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag());
}
long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();
@@ -1417,13 +1435,13 @@
// SearchSpec that wants to query every visible package.
Set<String> packageFilters = new ArraySet<>();
if (!searchSpec.getFilterPackageNames().isEmpty()) {
- if (searchSpec.getJoinSpec() == null) {
+ JoinSpec joinSpec = searchSpec.getJoinSpec();
+ if (joinSpec == null) {
packageFilters.addAll(searchSpec.getFilterPackageNames());
- } else if (!searchSpec.getJoinSpec().getNestedSearchSpec()
+ } else if (!joinSpec.getNestedSearchSpec()
.getFilterPackageNames().isEmpty()) {
packageFilters.addAll(searchSpec.getFilterPackageNames());
- packageFilters.addAll(
- searchSpec.getJoinSpec().getNestedSearchSpec().getFilterPackageNames());
+ packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames());
}
}
@@ -1445,14 +1463,14 @@
}
SearchSpecToProtoConverter searchSpecToProtoConverter =
new SearchSpecToProtoConverter(queryExpression, searchSpec, prefixFilters,
- mNamespaceMapLocked, mSchemaMapLocked, mConfig);
+ mNamespaceMapLocked, mSchemaCacheLocked, mConfig);
// Remove those inaccessible schemas.
searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
if (searchSpecToProtoConverter.hasNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return an
// empty SearchResult and skip sending request to Icing.
- return new SearchResultPage(Bundle.EMPTY);
+ return new SearchResultPage();
}
if (sStatsBuilder != null) {
sStatsBuilder.setAclCheckLatencyMillis(
@@ -1486,7 +1504,7 @@
long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto();
ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto(
- mNamespaceMapLocked, mSchemaMapLocked);
+ mNamespaceMapLocked, mSchemaCacheLocked);
ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
if (sStatsBuilder != null) {
sStatsBuilder.setRewriteSearchSpecLatencyMillis((int)
@@ -1500,7 +1518,7 @@
long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
// Rewrite search result before we return.
SearchResultPage searchResultPage = SearchResultToProtoConverter
- .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
+ .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig);
if (sStatsBuilder != null) {
sStatsBuilder.setRewriteSearchResultLatencyMillis(
(int) (SystemClock.elapsedRealtime()
@@ -1510,6 +1528,8 @@
}
@GuardedBy("mReadWriteLock")
+ // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging.
+ @SuppressWarnings("LiteProtoToString")
private SearchResultProto searchInIcingLocked(
@NonNull SearchSpecProto searchSpec,
@NonNull ResultSpecProto resultSpec,
@@ -1581,7 +1601,7 @@
searchSuggestionSpec,
Collections.singleton(prefix),
mNamespaceMapLocked,
- mSchemaMapLocked);
+ mSchemaCacheLocked);
if (searchSuggestionSpecToProtoConverter.hasNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return an
@@ -1616,7 +1636,7 @@
mReadWriteLock.readLock().lock();
try {
Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
- for (String prefix : mSchemaMapLocked.keySet()) {
+ for (String prefix : mSchemaCacheLocked.getAllPrefixes()) {
String packageName = getPackageName(prefix);
Set<String> databases = packageToDatabases.get(packageName);
@@ -1696,7 +1716,7 @@
long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
// Rewrite search result before we return.
SearchResultPage searchResultPage = SearchResultToProtoConverter
- .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
+ .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig);
if (sStatsBuilder != null) {
sStatsBuilder.setRewriteSearchResultLatencyMillis(
(int) (SystemClock.elapsedRealtime()
@@ -1910,7 +1930,7 @@
SearchSpecToProtoConverter searchSpecToProtoConverter =
new SearchSpecToProtoConverter(queryExpression, searchSpec,
- Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
+ Collections.singleton(prefix), mNamespaceMapLocked, mSchemaCacheLocked,
mConfig);
if (searchSpecToProtoConverter.hasNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return
@@ -2337,10 +2357,9 @@
}
for (String databaseName : databaseNames) {
String removedPrefix = createPrefix(packageName, databaseName);
- Map<String, SchemaTypeConfigProto> removedSchemas =
- Preconditions.checkNotNull(mSchemaMapLocked.remove(removedPrefix));
+ Set<String> removedSchemas = mSchemaCacheLocked.removePrefix(removedPrefix);
if (mVisibilityStoreLocked != null) {
- mVisibilityStoreLocked.removeVisibility(removedSchemas.keySet());
+ mVisibilityStoreLocked.removeVisibility(removedSchemas);
}
mNamespaceMapLocked.remove(removedPrefix);
@@ -2370,7 +2389,7 @@
resetResultProto.getStatus(),
resetResultProto);
mOptimizeIntervalCountLocked = 0;
- mSchemaMapLocked.clear();
+ mSchemaCacheLocked.clear();
mNamespaceMapLocked.clear();
mDocumentCountMapLocked.clear();
synchronized (mNextPageTokensLocked) {
@@ -2616,24 +2635,6 @@
values.add(prefixedValue);
}
- private static void addToMap(Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix,
- SchemaTypeConfigProto schemaTypeConfigProto) {
- Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
- if (schemaTypeMap == null) {
- schemaTypeMap = new ArrayMap<>();
- map.put(prefix, schemaTypeMap);
- }
- schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
- }
-
- private static void removeFromMap(Map<String, Map<String, SchemaTypeConfigProto>> map,
- String prefix, String schemaType) {
- Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
- if (schemaTypeMap != null) {
- schemaTypeMap.remove(schemaType);
- }
- }
-
/**
* Checks the given status code and throws an {@link AppSearchException} if code is an error.
*
@@ -2758,22 +2759,26 @@
}
if (LogUtil.DEBUG) {
if (Log.isLoggable(icingTag, Log.VERBOSE)) {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE, /*verbosity=*/
- (short) 1);
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE,
+ /*verbosity=*/ (short) 1);
return;
} else if (Log.isLoggable(icingTag, Log.DEBUG)) {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
return;
}
}
- if (Log.isLoggable(icingTag, Log.INFO)) {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
- } else if (Log.isLoggable(icingTag, Log.WARN)) {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);
+ if (LogUtil.INFO) {
+ if (Log.isLoggable(icingTag, Log.INFO)) {
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
+ return;
+ }
+ }
+ if (Log.isLoggable(icingTag, Log.WARN)) {
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);
} else if (Log.isLoggable(icingTag, Log.ERROR)) {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);
} else {
- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL);
+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL);
}
}
@@ -2795,11 +2800,7 @@
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;
+ return mSchemaCacheLocked.getAllPrefixedSchemaTypes();
} finally {
mReadWriteLock.readLock().unlock();
}
@@ -2814,7 +2815,7 @@
* {@link AppSearchResult} code.
* @return {@link AppSearchResult} error code
*/
- private static @AppSearchResult.ResultCode int statusProtoToResultCode(
+ @AppSearchResult.ResultCode private static int statusProtoToResultCode(
@NonNull StatusProto statusProto) {
return ResultCodeToProtoConverter.toResultCode(statusProto.getCode());
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
index 3e9e739..4e54764 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
@@ -23,15 +23,18 @@
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.stats.PutDocumentStats;
import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
import androidx.appsearch.localstorage.stats.SearchStats;
import androidx.appsearch.localstorage.stats.SetSchemaStats;
import androidx.appsearch.stats.SchemaMigrationStats;
+import java.util.List;
+
/**
* An interface for implementing client-defined logging AppSearch operations stats.
*
* <p>Any implementation needs to provide general information on how to log all the stats types.
- * (e.g. {@link CallStats})
+ * (for example {@link CallStats})
*
* <p>All implementations of this interface must be thread safe.
*
@@ -42,42 +45,79 @@
/**
* Logs {@link CallStats}
*/
- void logStats(@NonNull CallStats stats);
+ default void logStats(@NonNull CallStats stats) {
+ // no-op
+ }
/**
* Logs {@link PutDocumentStats}
*/
- void logStats(@NonNull PutDocumentStats stats);
+ default void logStats(@NonNull PutDocumentStats stats) {
+ // no-op
+ }
/**
* Logs {@link InitializeStats}
*/
- void logStats(@NonNull InitializeStats stats);
+ default void logStats(@NonNull InitializeStats stats) {
+ // no-op
+ }
/**
* Logs {@link SearchStats}
*/
- void logStats(@NonNull SearchStats stats);
+ default void logStats(@NonNull SearchStats stats) {
+ // no-op
+ }
- /**
- * Logs {@link RemoveStats}
- */
- void logStats(@NonNull RemoveStats stats);
+ /** Logs {@link RemoveStats} */
+ default void logStats(@NonNull RemoveStats stats) {
+ // no-op
+ }
/**
* Logs {@link OptimizeStats}
*/
- void logStats(@NonNull OptimizeStats stats);
+ default void logStats(@NonNull OptimizeStats stats) {
+ // no-op
+ }
/**
* Logs {@link SetSchemaStats}
*/
- void logStats(@NonNull SetSchemaStats stats);
+ default void logStats(@NonNull SetSchemaStats stats) {
+ // no-op
+ }
/**
* Logs {@link SchemaMigrationStats}
*/
- void logStats(@NonNull SchemaMigrationStats stats);
+ default void logStats(@NonNull SchemaMigrationStats stats) {
+ // no-op
+ }
+
+ /**
+ * Logs a list of {@link SearchSessionStats}.
+ *
+ * <p>Since the client app may report search intents belonging to different search sessions in a
+ * single taken action reporting request, the stats extractor will separate them into multiple
+ * search sessions. Therefore, we need a list of {@link SearchSessionStats} here.
+ *
+ * <p>For example, the client app reports the following search intent sequence:
+ *
+ * <ul>
+ * <li>t = 1, the user searches "a" with some clicks.
+ * <li>t = 5, the user searches "app" with some clicks.
+ * <li>t = 10000, the user searches "email" with some clicks.
+ * </ul>
+ *
+ * The extractor will detect "email" belongs to a completely independent search session, and
+ * creates 2 {@link SearchSessionStats} with search intents ["a", "app"] and ["email"]
+ * respectively.
+ */
+ default void logStats(@NonNull List<SearchSessionStats> searchSessionsStats) {
+ // no-op
+ }
// TODO(b/173532925) Add remaining logStats once we add all the stats.
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
index 652fc94..74ed1c2 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -169,11 +169,9 @@
Preconditions.checkNotNull(fromNativeStats);
Preconditions.checkNotNull(toStatsBuilder);
- @SuppressWarnings("deprecation")
- int deleteType = DeleteStatsProto.DeleteType.Code.DEPRECATED_QUERY.getNumber();
toStatsBuilder
.setNativeLatencyMillis(fromNativeStats.getLatencyMs())
- .setDeleteType(deleteType)
+ .setDeleteType(RemoveStats.QUERY)
.setDeletedDocumentCount(fromNativeStats.getNumDocumentsDeleted());
}
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 659433a..e040e50 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
@@ -19,7 +19,6 @@
import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
-import android.os.Bundle;
import android.os.Parcel;
import androidx.annotation.NonNull;
@@ -32,6 +31,7 @@
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
import androidx.appsearch.stats.SchemaMigrationStats;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -130,11 +130,11 @@
+ newDocument.getSchemaType()
+ ". But the schema types doesn't exist in the request");
}
- Bundle bundle = newDocument.getBundle();
+ GenericDocumentParcel documentParcel = newDocument.getDocumentParcel();
byte[] serializedMessage;
Parcel parcel = Parcel.obtain();
try {
- parcel.writeBundle(bundle);
+ documentParcel.writeToParcel(parcel, /* flags= */ 0);
serializedMessage = parcel.marshall();
} finally {
parcel.recycle();
@@ -224,17 +224,17 @@
@NonNull CodedInputStream codedInputStream) throws IOException {
byte[] serializedMessage = codedInputStream.readByteArray();
- Bundle bundle;
+ GenericDocumentParcel documentParcel;
Parcel parcel = Parcel.obtain();
try {
parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
parcel.setDataPosition(0);
- bundle = parcel.readBundle();
+ documentParcel = GenericDocumentParcel.CREATOR.createFromParcel(parcel);
} finally {
parcel.recycle();
}
- return new GenericDocument(bundle);
+ return new GenericDocument(documentParcel);
}
@Override
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 ef8ec3b..7597399 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
@@ -93,7 +93,7 @@
AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
new AppSearchBatchResult.Builder<>();
- Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+ Map<String, List<String>> typePropertyPaths = request.getProjections();
CallerAccess access = new CallerAccess(mContext.getPackageName());
for (String id : request.getIds()) {
try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
index 870df5a..d3246bf 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -36,7 +36,7 @@
boolean DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT = false;
- float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.0f;
+ float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.9f;
/**
* The default compression level in IcingSearchEngineOptions proto matches the
@@ -62,7 +62,7 @@
*/
int DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD = 65536;
- boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = false;
+ boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = true;
/**
* The default sort threshold for the lite index when sort at indexing is enabled.
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 db55481..bc9715b 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
@@ -26,6 +26,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.exceptions.AppSearchException;
@@ -39,7 +40,6 @@
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
/**
* An AppSearch storage system which stores data locally in the app's storage space using a bundled
@@ -56,8 +56,6 @@
public class LocalStorage {
private static final String TAG = "AppSearchLocalStorage";
- private static final String ICING_LIB_ROOT_DIR = "appsearch";
-
/** Contains information about how to create the search session. */
public static final class SearchContext {
final Context mContext;
@@ -249,7 +247,8 @@
// AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
// mutate requests will need to gain write lock and query requests need to gain read lock.
- static final Executor EXECUTOR = Executors.newCachedThreadPool();
+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+ .createCachedThreadPoolExecutor();
private static volatile LocalStorage sInstance;
@@ -326,7 +325,8 @@
@Nullable AppSearchLogger logger)
throws AppSearchException {
Preconditions.checkNotNull(context);
- File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
+ File icingDir = AppSearchEnvironmentFactory.getEnvironmentInstance()
+ .getAppSearchDir(context, /* userHandle= */ null);
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
InitializeStats.Builder initStatsBuilder = null;
@@ -346,8 +346,8 @@
/* shouldRetrieveParentInfo= */ true
),
initStatsBuilder,
- new JetpackOptimizeStrategy(),
- /*visibilityChecker=*/null);
+ /*visibilityChecker=*/ null,
+ new JetpackOptimizeStrategy());
if (logger != null) {
initStatsBuilder.setTotalLatencyMillis(
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
index 74ea1c7..11b530e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
@@ -96,6 +96,6 @@
@Override
public boolean getBuildPropertyExistenceMetadataHits() {
- return DEFAULT_BUILD_PROPERTY_EXISTENCE_METADATA_HITS;
+ return true;
}
}
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 d7c46ef..907a078 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
@@ -31,6 +31,7 @@
import androidx.appsearch.observer.ObserverCallback;
import androidx.appsearch.observer.ObserverSpec;
import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.appsearch.util.ExceptionUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.ObjectsCompat;
@@ -75,8 +76,12 @@
@Override
public boolean equals(@Nullable Object o) {
- if (this == o) return true;
- if (!(o instanceof DocumentChangeGroupKey)) return false;
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DocumentChangeGroupKey)) {
+ return false;
+ }
DocumentChangeGroupKey that = (DocumentChangeGroupKey) o;
return mPackageName.equals(that.mPackageName)
&& mDatabaseName.equals(that.mDatabaseName)
@@ -410,8 +415,9 @@
try {
observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
- } catch (Throwable t) {
- Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+ ExceptionUtil.handleException(e);
}
}
}
@@ -429,8 +435,9 @@
try {
observerInfo.mObserverCallback.onDocumentChanged(documentChangeInfo);
- } catch (Throwable t) {
- Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+ ExceptionUtil.handleException(e);
}
}
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
new file mode 100644
index 0000000..7138182
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2024 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;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Caches and manages schema information for AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaCache {
+ /**
+ * A map that contains schema types and SchemaTypeConfigProtos for all package-database
+ * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+ * prefixed schema type to its respective SchemaTypeConfigProto.
+ */
+ private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap = new ArrayMap<>();
+
+ /**
+ * A map that contains schema types and all children schema types for all package-database
+ * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+ * prefixed schema type to its respective list of children prefixed schema types.
+ */
+ private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap =
+ new ArrayMap<>();
+
+ public SchemaCache() {
+ }
+
+ public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ mSchemaMap.putAll(Preconditions.checkNotNull(schemaMap));
+ rebuildSchemaParentToChildrenMap();
+ }
+
+ /**
+ * Returns the schema map for the given prefix.
+ */
+ @NonNull
+ public Map<String, SchemaTypeConfigProto> getSchemaMapForPrefix(@NonNull String prefix) {
+ Preconditions.checkNotNull(prefix);
+
+ Map<String, SchemaTypeConfigProto> schemaMap = mSchemaMap.get(prefix);
+ if (schemaMap == null) {
+ return Collections.emptyMap();
+ }
+ return schemaMap;
+ }
+
+ /**
+ * Returns a set of all prefixes stored in the cache.
+ */
+ @NonNull
+ public Set<String> getAllPrefixes() {
+ return Collections.unmodifiableSet(mSchemaMap.keySet());
+ }
+
+ /**
+ * Returns all prefixed schema types stored in the cache.
+ *
+ * <p>This method is inefficient to call repeatedly.
+ */
+ @NonNull
+ public List<String> getAllPrefixedSchemaTypes() {
+ List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
+ for (Map<String, SchemaTypeConfigProto> value : mSchemaMap.values()) {
+ cachedPrefixedSchemaTypes.addAll(value.keySet());
+ }
+ return cachedPrefixedSchemaTypes;
+ }
+
+ /**
+ * Returns the schema types for the given set of prefixed schema types with their
+ * descendants, based on the schema parent-to-children map held in the cache.
+ */
+ @NonNull
+ public Set<String> getSchemaTypesWithDescendants(@NonNull String prefix,
+ @NonNull Set<String> prefixedSchemaTypes) {
+ Preconditions.checkNotNull(prefix);
+ Preconditions.checkNotNull(prefixedSchemaTypes);
+ Map<String, List<String>> parentToChildrenMap = mSchemaParentToChildrenMap.get(prefix);
+ if (parentToChildrenMap == null) {
+ parentToChildrenMap = Collections.emptyMap();
+ }
+
+ // Perform a BFS search on the inheritance graph started by the set of prefixedSchemaTypes.
+ Set<String> visited = new ArraySet<>();
+ Queue<String> prefixedSchemaQueue = new ArrayDeque<>(prefixedSchemaTypes);
+ while (!prefixedSchemaQueue.isEmpty()) {
+ String currentPrefixedSchema = prefixedSchemaQueue.poll();
+ if (visited.contains(currentPrefixedSchema)) {
+ continue;
+ }
+ visited.add(currentPrefixedSchema);
+ List<String> children = parentToChildrenMap.get(currentPrefixedSchema);
+ if (children == null) {
+ continue;
+ }
+ prefixedSchemaQueue.addAll(children);
+ }
+
+ return visited;
+ }
+
+ /**
+ * Rebuilds the schema parent-to-children map for the given prefix, based on the current
+ * schema map.
+ *
+ * <p>The schema parent-to-children map is required to be updated when
+ * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
+ * results from {@link #getSchemaTypesWithDescendants} would be stale.
+ */
+ public void rebuildSchemaParentToChildrenMapForPrefix(@NonNull String prefix) {
+ Preconditions.checkNotNull(prefix);
+
+ mSchemaParentToChildrenMap.remove(prefix);
+ Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix);
+ if (prefixedSchemaMap == null) {
+ return;
+ }
+
+ // Build the parent-to-children map for the current prefix.
+ Map<String, List<String>> parentToChildrenMap = new ArrayMap<>();
+ for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) {
+ for (int i = 0; i < childSchemaConfig.getParentTypesCount(); i++) {
+ String parent = childSchemaConfig.getParentTypes(i);
+ List<String> children = parentToChildrenMap.get(parent);
+ if (children == null) {
+ children = new ArrayList<>();
+ parentToChildrenMap.put(parent, children);
+ }
+ children.add(childSchemaConfig.getSchemaType());
+ }
+ }
+
+ // Record the map for the current prefix.
+ if (!parentToChildrenMap.isEmpty()) {
+ mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap);
+ }
+ }
+
+ /**
+ * Rebuilds the schema parent-to-children map based on the current schema map.
+ *
+ * <p>The schema parent-to-children map is required to be updated when
+ * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
+ * results from {@link #getSchemaTypesWithDescendants} would be stale.
+ */
+ public void rebuildSchemaParentToChildrenMap() {
+ mSchemaParentToChildrenMap.clear();
+ for (String prefix : mSchemaMap.keySet()) {
+ rebuildSchemaParentToChildrenMapForPrefix(prefix);
+ }
+ }
+
+ /**
+ * Adds a schema to the schema map.
+ *
+ * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
+ * and either {@link #rebuildSchemaParentToChildrenMap} or
+ * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
+ * cache.
+ */
+ public void addToSchemaMap(@NonNull String prefix,
+ @NonNull SchemaTypeConfigProto schemaTypeConfigProto) {
+ Preconditions.checkNotNull(prefix);
+ Preconditions.checkNotNull(schemaTypeConfigProto);
+
+ Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+ if (schemaTypeMap == null) {
+ schemaTypeMap = new ArrayMap<>();
+ mSchemaMap.put(prefix, schemaTypeMap);
+ }
+ schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
+ }
+
+ /**
+ * Removes a schema from the schema map.
+ *
+ * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
+ * and either {@link #rebuildSchemaParentToChildrenMap} or
+ * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
+ * cache.
+ */
+ public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) {
+ Preconditions.checkNotNull(prefix);
+ Preconditions.checkNotNull(schemaType);
+
+ Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+ if (schemaTypeMap != null) {
+ schemaTypeMap.remove(schemaType);
+ }
+ }
+
+ /**
+ * Removes the entry of the given prefix from both the schema map and the schema
+ * parent-to-children map, and returns the set of removed prefixed schema type.
+ */
+ @NonNull
+ public Set<String> removePrefix(@NonNull String prefix) {
+ Preconditions.checkNotNull(prefix);
+
+ Map<String, SchemaTypeConfigProto> removedSchemas =
+ Preconditions.checkNotNull(mSchemaMap.remove(prefix));
+ mSchemaParentToChildrenMap.remove(prefix);
+ return removedSchemas.keySet();
+ }
+
+ /**
+ * Clears all data in the cache.
+ */
+ public void clear() {
+ mSchemaMap.clear();
+ mSchemaParentToChildrenMap.clear();
+ }
+}
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 36644b4..8094be0 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
@@ -34,6 +34,7 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
@@ -45,7 +46,6 @@
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;
@@ -80,7 +80,6 @@
private final AppSearchImpl mAppSearchImpl;
private final Executor mExecutor;
private final Features mFeatures;
- private final Context mContext;
private final String mDatabaseName;
@Nullable private final AppSearchLogger mLogger;
@@ -100,11 +99,11 @@
mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
mExecutor = Preconditions.checkNotNull(executor);
mFeatures = Preconditions.checkNotNull(features);
- mContext = Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(context);
mDatabaseName = Preconditions.checkNotNull(databaseName);
mLogger = logger;
- mPackageName = mContext.getPackageName();
+ mPackageName = context.getPackageName();
mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
}
@@ -126,15 +125,15 @@
mPackageName, mDatabaseName);
}
- // Extract a Map<schema, VisibilityDocument> from the request.
- List<VisibilityDocument> visibilityDocuments = VisibilityDocument
- .toVisibilityDocuments(request);
+ List<InternalVisibilityConfig> visibilityConfigs =
+ InternalVisibilityConfig.toInternalVisibilityConfigs(request);
Map<String, Migrator> migrators = request.getMigrators();
// No need to trigger migration if user never set migrator.
- if (migrators.size() == 0) {
+ if (migrators.isEmpty()) {
SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
- visibilityDocuments, firstSetSchemaStatsBuilder);
+ visibilityConfigs,
+ firstSetSchemaStatsBuilder);
long dispatchNotificationStartTimeMillis = SystemClock.elapsedRealtime();
// Schedule a task to dispatch change notifications. See requirements for where the
@@ -172,9 +171,9 @@
Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
getSchemaResponse.getSchemas(), migrators, currentVersion, finalVersion);
// No need to trigger migration if no migrator is active.
- if (activeMigrators.size() == 0) {
+ if (activeMigrators.isEmpty()) {
SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
- visibilityDocuments, firstSetSchemaStatsBuilder);
+ visibilityConfigs, firstSetSchemaStatsBuilder);
if (firstSetSchemaStatsBuilder != null) {
firstSetSchemaStatsBuilder.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - startMillis));
@@ -194,7 +193,7 @@
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- visibilityDocuments,
+ visibilityConfigs,
/*forceOverride=*/false,
request.getVersion(),
firstSetSchemaStatsBuilder);
@@ -233,7 +232,7 @@
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- visibilityDocuments,
+ visibilityConfigs,
/*forceOverride=*/ true,
request.getVersion(),
secondSetSchemaStatsBuilder);
@@ -246,9 +245,8 @@
}
}
long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
- SetSchemaResponse.Builder responseBuilder = internalSetSchemaResponse
- .getSetSchemaResponse()
- .toBuilder()
+ SetSchemaResponse.Builder responseBuilder = new SetSchemaResponse.Builder(
+ internalSetSchemaResponse.getSetSchemaResponse())
.addMigratedTypes(activeMigrators.keySet());
mIsMutated = true;
@@ -352,23 +350,26 @@
@NonNull PutDocumentsRequest request) {
Preconditions.checkNotNull(request);
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+
+ List<GenericDocument> documents = request.getGenericDocuments();
+ List<GenericDocument> takenActions = request.getTakenActionGenericDocuments();
+
ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
- for (int i = 0; i < request.getGenericDocuments().size(); i++) {
- GenericDocument document = request.getGenericDocuments().get(i);
- try {
- mAppSearchImpl.putDocument(
- mPackageName,
- mDatabaseName,
- document,
- /*sendChangeNotifications=*/ true,
- mLogger);
- resultBuilder.setSuccess(document.getId(), /*value=*/ null);
- } catch (Throwable t) {
- resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
- }
+
+ // Normal documents.
+ for (int i = 0; i < documents.size(); i++) {
+ GenericDocument document = documents.get(i);
+ putGenericDocument(document, resultBuilder);
}
+
+ // TakenAction documents.
+ for (int i = 0; i < takenActions.size(); i++) {
+ GenericDocument takenActionGenericDocument = takenActions.get(i);
+ putGenericDocument(takenActionGenericDocument, resultBuilder);
+ }
+
// Now that the batch has been written. Persist the newly written data.
mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
mIsMutated = true;
@@ -382,7 +383,7 @@
// The existing documents with same ID will be deleted, so there may be some resources that
// could be released after optimize().
- checkForOptimize(/*mutateBatchSize=*/ request.getGenericDocuments().size());
+ checkForOptimize(/*mutateBatchSize=*/ documents.size() + takenActions.size());
return future;
}
@@ -396,7 +397,7 @@
AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
new AppSearchBatchResult.Builder<>();
- Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+ Map<String, List<String>> typePropertyPaths = request.getProjections();
for (String id : request.getIds()) {
try {
GenericDocument document =
@@ -583,7 +584,7 @@
* forceoverride in the request.
*/
private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
- @NonNull List<VisibilityDocument> visibilityDocuments,
+ @NonNull List<InternalVisibilityConfig> visibilityConfigs,
@Nullable SetSchemaStats.Builder setSchemaStatsBuilder)
throws AppSearchException {
if (setSchemaStatsBuilder != null) {
@@ -593,7 +594,7 @@
mPackageName,
mDatabaseName,
new ArrayList<>(request.getSchemas()),
- visibilityDocuments,
+ visibilityConfigs,
request.isForceOverride(),
request.getVersion(),
setSchemaStatsBuilder);
@@ -621,6 +622,28 @@
mAppSearchImpl.dispatchAndClearChangeNotifications();
}
+ /**
+ * Calls {@link AppSearchImpl} to put a generic document and sets the result.
+ *
+ * @param document the {@link GenericDocument} to put.
+ * @param resultBuilder an {@link AppSearchBatchResult.Builder} object for collecting the
+ * result.
+ */
+ private void putGenericDocument(
+ GenericDocument document, AppSearchBatchResult.Builder<String, Void> resultBuilder) {
+ try {
+ mAppSearchImpl.putDocument(
+ mPackageName,
+ mDatabaseName,
+ document,
+ /*sendChangeNotifications=*/ true,
+ mLogger);
+ resultBuilder.setSuccess(document.getId(), /*value=*/ null);
+ } catch (Throwable t) {
+ resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
+ }
+ }
+
private void checkForOptimize(int mutateBatchSize) {
mExecutor.execute(() -> {
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index aedd191..2320d8b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchConfig;
@@ -54,6 +55,8 @@
private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
+ private static final EmbeddingVector[] EMPTY_EMBEDDING_ARRAY =
+ new EmbeddingVector[0];
private GenericDocumentToProtoConverter() {
}
@@ -109,6 +112,12 @@
DocumentProto proto = toDocumentProto(documentValues[j]);
propertyProto.addDocumentValues(proto);
}
+ } else if (property instanceof EmbeddingVector[]) {
+ EmbeddingVector[] embeddingValues = (EmbeddingVector[]) property;
+ for (int j = 0; j < embeddingValues.length; j++) {
+ propertyProto.addVectorValues(
+ embeddingVectorToVectorProto(embeddingValues[j]));
+ }
} else if (property == null) {
throw new IllegalStateException(
String.format("Property \"%s\" doesn't have any value!", name));
@@ -205,6 +214,13 @@
schemaTypeMap, config);
}
documentBuilder.setPropertyDocument(name, values);
+ } else if (property.getVectorValuesCount() > 0) {
+ EmbeddingVector[] values =
+ new EmbeddingVector[property.getVectorValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = vectorProtoToEmbeddingVector(property.getVectorValues(j));
+ }
+ documentBuilder.setPropertyEmbedding(name, values);
} else {
// TODO(b/184966497): Optimize by caching PropertyConfigProto
SchemaTypeConfigProto schema =
@@ -216,6 +232,37 @@
}
/**
+ * Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}.
+ */
+ @NonNull
+ public static EmbeddingVector vectorProtoToEmbeddingVector(
+ @NonNull PropertyProto.VectorProto vectorProto) {
+ Preconditions.checkNotNull(vectorProto);
+
+ float[] values = new float[vectorProto.getValuesCount()];
+ for (int i = 0; i < vectorProto.getValuesCount(); i++) {
+ values[i] = vectorProto.getValues(i);
+ }
+ return new EmbeddingVector(values, vectorProto.getModelSignature());
+ }
+
+ /**
+ * Converts an {@link EmbeddingVector} into a {@link PropertyProto.VectorProto}.
+ */
+ @NonNull
+ public static PropertyProto.VectorProto embeddingVectorToVectorProto(
+ @NonNull EmbeddingVector embedding) {
+ Preconditions.checkNotNull(embedding);
+
+ PropertyProto.VectorProto.Builder builder = PropertyProto.VectorProto.newBuilder();
+ for (int i = 0; i < embedding.getValues().length; i++) {
+ builder.addValues(embedding.getValues()[i]);
+ }
+ builder.setModelSignature(embedding.getModelSignature());
+ return builder.build();
+ }
+
+ /**
* Get the list of unprefixed parent type names of {@code prefixedSchemaType}.
*
* <p>It's guaranteed that child types always appear before parent types in the list.
@@ -305,6 +352,9 @@
case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING:
+ documentBuilder.setPropertyEmbedding(propertyName, EMPTY_EMBEDDING_ARRAY);
+ break;
default:
throw new IllegalStateException("Unknown type of value: " + propertyName);
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
index 2106917..eedbb0d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
@@ -35,7 +35,7 @@
private ResultCodeToProtoConverter() {}
/** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
- public static @AppSearchResult.ResultCode int toResultCode(
+ @AppSearchResult.ResultCode public static int toResultCode(
@NonNull StatusProto.Code statusCode) {
switch (statusCode) {
case OK:
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index e21213f..5014f04 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -24,6 +24,7 @@
import androidx.core.util.Preconditions;
import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
import com.google.android.icing.proto.IntegerIndexingConfig;
import com.google.android.icing.proto.JoinableConfig;
import com.google.android.icing.proto.PropertyConfigProto;
@@ -55,6 +56,7 @@
Preconditions.checkNotNull(schema);
SchemaTypeConfigProto.Builder protoBuilder = SchemaTypeConfigProto.newBuilder()
.setSchemaType(schema.getSchemaType())
+ .setDescription(schema.getDescription())
.setVersion(version);
List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
for (int i = 0; i < properties.size(); i++) {
@@ -70,7 +72,8 @@
@NonNull AppSearchSchema.PropertyConfig property) {
Preconditions.checkNotNull(property);
PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
- .setPropertyName(property.getName());
+ .setPropertyName(property.getName())
+ .setDescription(property.getDescription());
// Set dataType
@AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -101,11 +104,6 @@
.setValueType(
convertJoinableValueTypeToProto(
stringProperty.getJoinableValueType()))
- // @exportToFramework:startStrip()
- // Do not call this in framework as it will populate the proto field and
- // fail comparison tests.
- .setPropagateDelete(stringProperty.getDeletionPropagation())
- // @exportToFramework:endStrip()
.build();
builder.setJoinableConfig(joinableConfig);
}
@@ -140,6 +138,20 @@
.build();
builder.setIntegerIndexingConfig(integerIndexingConfig);
}
+ } else if (property instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+ AppSearchSchema.EmbeddingPropertyConfig embeddingProperty =
+ (AppSearchSchema.EmbeddingPropertyConfig) property;
+ // Set embedding indexing config only if it is indexable (i.e. not INDEXING_TYPE_NONE).
+ // Non-indexable embedding property only requires to builder.setDataType, without the
+ // need to set an EmbeddingIndexingConfig.
+ if (embeddingProperty.getIndexingType()
+ != AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE) {
+ EmbeddingIndexingConfig embeddingIndexingConfig =
+ EmbeddingIndexingConfig.newBuilder().setEmbeddingIndexingType(
+ convertEmbeddingIndexingTypeToProto(
+ embeddingProperty.getIndexingType())).build();
+ builder.setEmbeddingIndexingConfig(embeddingIndexingConfig);
+ }
}
return builder.build();
}
@@ -154,6 +166,7 @@
Preconditions.checkNotNull(proto);
AppSearchSchema.Builder builder =
new AppSearchSchema.Builder(proto.getSchemaType());
+ builder.setDescription(proto.getDescription());
List<PropertyConfigProto> properties = proto.getPropertiesList();
for (int i = 0; i < properties.size(); i++) {
AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -177,20 +190,26 @@
return toLongPropertyConfig(proto);
case DOUBLE:
return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber())
.build();
case BOOLEAN:
return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber())
.build();
case BYTES:
return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber())
.build();
case DOCUMENT:
return toDocumentPropertyConfig(proto);
+ case VECTOR:
+ return toEmbeddingPropertyConfig(proto);
default:
- throw new IllegalArgumentException("Invalid dataType: " + proto.getDataType());
+ throw new IllegalArgumentException(
+ "Invalid dataType code: " + proto.getDataType().getNumber());
}
}
@@ -199,11 +218,11 @@
@NonNull PropertyConfigProto proto) {
AppSearchSchema.StringPropertyConfig.Builder builder =
new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber())
.setJoinableValueType(
convertJoinableValueTypeFromProto(
proto.getJoinableConfig().getValueType()))
- .setDeletionPropagation(proto.getJoinableConfig().getPropagateDelete())
.setTokenizerType(
proto.getStringIndexingConfig().getTokenizerType().getNumber());
@@ -220,6 +239,7 @@
AppSearchSchema.DocumentPropertyConfig.Builder builder =
new AppSearchSchema.DocumentPropertyConfig.Builder(
proto.getPropertyName(), proto.getSchemaType())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber())
.setShouldIndexNestedProperties(
proto.getDocumentIndexingConfig().getIndexNestedProperties());
@@ -233,6 +253,7 @@
@NonNull PropertyConfigProto proto) {
AppSearchSchema.LongPropertyConfig.Builder builder =
new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
+ .setDescription(proto.getDescription())
.setCardinality(proto.getCardinality().getNumber());
// Set indexingType
@@ -244,6 +265,21 @@
}
@NonNull
+ private static AppSearchSchema.EmbeddingPropertyConfig toEmbeddingPropertyConfig(
+ @NonNull PropertyConfigProto proto) {
+ AppSearchSchema.EmbeddingPropertyConfig.Builder builder =
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder(proto.getPropertyName())
+ .setCardinality(proto.getCardinality().getNumber());
+
+ // Set indexingType
+ EmbeddingIndexingConfig.EmbeddingIndexingType.Code embeddingIndexingType =
+ proto.getEmbeddingIndexingConfig().getEmbeddingIndexingType();
+ builder.setIndexingType(convertEmbeddingIndexingTypeFromProto(embeddingIndexingType));
+
+ return builder.build();
+ }
+
+ @NonNull
private static JoinableConfig.ValueType.Code convertJoinableValueTypeToProto(
@AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType) {
switch (joinableValueType) {
@@ -265,12 +301,11 @@
return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
case QUALIFIED_ID:
return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
- default:
- // Avoid crashing in the 'read' path; we should try to interpret the document to the
- // extent possible.
- Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
- return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
}
+ // Avoid crashing in the 'read' path; we should try to interpret the document to the
+ // extent possible.
+ Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
+ return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
}
@NonNull
@@ -344,4 +379,34 @@
return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
}
}
+
+ @NonNull
+ private static EmbeddingIndexingConfig.EmbeddingIndexingType.Code
+ convertEmbeddingIndexingTypeToProto(
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+ switch (indexingType) {
+ case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE:
+ return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.UNKNOWN;
+ case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY:
+ return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.LINEAR_SEARCH;
+ default:
+ throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+ }
+ }
+
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+ private static int convertEmbeddingIndexingTypeFromProto(
+ @NonNull EmbeddingIndexingConfig.EmbeddingIndexingType.Code indexingType) {
+ switch (indexingType) {
+ case UNKNOWN:
+ return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+ case LINEAR_SEARCH:
+ return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY;
+ default:
+ // Avoid crashing in the 'read' path; we should try to interpret the document to the
+ // extent possible.
+ Log.w(TAG, "Invalid indexingType: " + indexingType.getNumber());
+ return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+ }
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 8652cca..b88eccd 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -20,8 +20,6 @@
import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
-import android.os.Bundle;
-
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.AppSearchResult;
@@ -30,7 +28,7 @@
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchConfig;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.localstorage.SchemaCache;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -39,6 +37,7 @@
import com.google.android.icing.proto.SnippetProto;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
/**
@@ -55,24 +54,20 @@
* Translate a {@link SearchResultProto} into {@link SearchResultPage}.
*
* @param proto The {@link SearchResultProto} containing results.
- * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
- * stores all existing prefixed schema type.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @return {@link SearchResultPage} of results.
*/
@NonNull
public static SearchResultPage toSearchResultPage(@NonNull SearchResultProto proto,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
- @NonNull AppSearchConfig config)
+ @NonNull SchemaCache schemaCache, @NonNull AppSearchConfig config)
throws AppSearchException {
- Bundle bundle = new Bundle();
- bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
- ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
+ List<SearchResult> results = new ArrayList<>(proto.getResultsCount());
for (int i = 0; i < proto.getResultsCount(); i++) {
- SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap, config);
- resultBundles.add(result.getBundle());
+ SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaCache,
+ config);
+ results.add(result);
}
- bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
- return new SearchResultPage(bundle);
+ return new SearchResultPage(proto.getNextPageToken(), results);
}
/**
@@ -80,26 +75,28 @@
* database prefix will be removed from {@link GenericDocument}.
*
* @param proto The proto to be converted.
- * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
- * stores all existing prefixed schema type.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @return A {@link SearchResult}.
*/
@NonNull
private static SearchResult toUnprefixedSearchResult(
@NonNull SearchResultProto.ResultProto proto,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull AppSearchConfig config) throws AppSearchException {
DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
String prefix = removePrefixesFromDocument(documentBuilder);
Map<String, SchemaTypeConfigProto> schemaTypeMap =
- Preconditions.checkNotNull(schemaMap.get(prefix));
+ schemaCache.getSchemaMapForPrefix(prefix);
GenericDocument document =
GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
schemaTypeMap, config);
SearchResult.Builder builder =
new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
.setGenericDocument(document).setRankingSignal(proto.getScore());
+ for (int i = 0; i < proto.getAdditionalScoresCount(); i++) {
+ builder.addInformationalRankingSignal(proto.getAdditionalScores(i));
+ }
if (proto.hasSnippet()) {
for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i);
@@ -118,7 +115,8 @@
"Nesting joined results within joined results not allowed.");
}
- builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap, config));
+ builder.addJoinedResult(
+ toUnprefixedSearchResult(joinedResultProto, schemaCache, config));
}
return builder.build();
}
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 42cfd44..ad5be47 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
@@ -26,11 +26,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.FeatureConstants;
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.IcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
@@ -92,11 +95,9 @@
private final Map<String, Set<String>> mNamespaceMap;
/**
- *The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>} stores all
- * prefixed schema filters which are stored inAppSearch. This is a field so that we can
- * generated nested protos.
+ * The SchemaCache instance held in AppSearch.
*/
- private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap;
+ private final SchemaCache mSchemaCache;
/**
* Optional config flags in {@link SearchSpecProto}.
@@ -120,21 +121,20 @@
* database prefixes are allowed, so nothing will be searched.
* @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
* all prefixed namespace filters which are stored in AppSearch.
- * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
- * stores all prefixed schema filters which are stored inAppSearch.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
*/
public SearchSpecToProtoConverter(
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec,
@NonNull Set<String> allAllowedPrefixes,
@NonNull Map<String, Set<String>> namespaceMap,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull IcingOptionsConfig icingOptionsConfig) {
mQueryExpression = Preconditions.checkNotNull(queryExpression);
mSearchSpec = Preconditions.checkNotNull(searchSpec);
mAllAllowedPrefixes = Preconditions.checkNotNull(allAllowedPrefixes);
mNamespaceMap = Preconditions.checkNotNull(namespaceMap);
- mSchemaMap = Preconditions.checkNotNull(schemaMap);
+ mSchemaCache = Preconditions.checkNotNull(schemaCache);
mIcingOptionsConfig = Preconditions.checkNotNull(icingOptionsConfig);
// This field holds the prefix filters for the SearchSpec currently being handled, which
@@ -169,7 +169,7 @@
if (!mTargetPrefixedNamespaceFilters.isEmpty()) {
mTargetPrefixedSchemaFilters =
SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
- mCurrentSearchSpecPrefixFilters, schemaMap,
+ mCurrentSearchSpecPrefixFilters, schemaCache,
searchSpec.getFilterSchemas());
} else {
mTargetPrefixedSchemaFilters = new ArraySet<>();
@@ -185,17 +185,17 @@
joinSpec.getNestedSearchSpec(),
mAllAllowedPrefixes,
namespaceMap,
- schemaMap,
+ schemaCache,
mIcingOptionsConfig);
}
/**
- * @return whether this search's target filters are empty. If any target filter is empty, we
+ * Returns whether this search's target filters are empty. If any target filter is empty, we
* should skip send request to Icing.
*
- * <p> The nestedConverter is not checked as {@link SearchResult}s from the nested query have
- * to be joined to a {@link SearchResult} from the parent query. If the parent query has
- * nothing to search, then so does the child query.
+ * <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have to
+ * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to
+ * search, then so does the child query.
*/
public boolean hasNothingToSearch() {
return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
@@ -291,6 +291,13 @@
.addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
.setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch());
+ List<EmbeddingVector> searchEmbeddings = mSearchSpec.getSearchEmbeddings();
+ for (int i = 0; i < searchEmbeddings.size(); i++) {
+ protoBuilder.addEmbeddingQueryVectors(
+ GenericDocumentToProtoConverter.embeddingVectorToVectorProto(
+ searchEmbeddings.get(i)));
+ }
+
// Convert type property filter map into type property mask proto.
for (Map.Entry<String, List<String>> entry :
mSearchSpec.getFilterProperties().entrySet()) {
@@ -319,11 +326,22 @@
}
protoBuilder.setTermMatchType(termMatchCodeProto);
+ @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType =
+ mSearchSpec.getDefaultEmbeddingSearchMetricType();
+ SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto =
+ SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType);
+ if (embeddingSearchMetricTypeProto == null || embeddingSearchMetricTypeProto.equals(
+ SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) {
+ throw new IllegalArgumentException(
+ "Invalid embedding search metric type: " + embeddingSearchMetricType);
+ }
+ protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto);
+
if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) {
JoinSpecProto.NestedSpecProto nestedSpec =
JoinSpecProto.NestedSpecProto.newBuilder()
.setResultSpec(mNestedConverter.toResultSpecProto(
- mNamespaceMap, mSchemaMap))
+ mNamespaceMap, mSchemaCache))
.setScoringSpec(mNestedConverter.toScoringSpecProto())
.setSearchSpec(mNestedConverter.toSearchSpecProto())
.build();
@@ -342,18 +360,18 @@
protoBuilder.setJoinSpec(joinSpecProtoBuilder);
}
- // TODO(b/208654892) Remove this field once EXPERIMENTAL_ICING_ADVANCED_QUERY is fully
- // supported.
- boolean turnOnIcingAdvancedQuery =
- mSearchSpec.isNumericSearchEnabled() || mSearchSpec.isVerbatimSearchEnabled()
- || mSearchSpec.isListFilterQueryLanguageEnabled();
- if (turnOnIcingAdvancedQuery) {
- protoBuilder.setSearchType(
- SearchSpecProto.SearchType.Code.EXPERIMENTAL_ICING_ADVANCED_QUERY);
+ if (mSearchSpec.isListFilterHasPropertyFunctionEnabled()
+ && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) {
+ // This condition should never be reached as long as Features.isFeatureSupported() is
+ // consistent with IcingOptionsConfig.
+ throw new UnsupportedOperationException(
+ FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION
+ + " is currently not operational because the building process for the "
+ + "associated metadata has not yet been turned on.");
}
// Set enabled features
- protoBuilder.addAllEnabledFeatures(mSearchSpec.getEnabledFeatures());
+ protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(mSearchSpec.getEnabledFeatures()));
return protoBuilder.build();
}
@@ -390,14 +408,12 @@
*
* @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
* all existing prefixed namespace.
- * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType,
- * schemaProto>>} stores all prefixed schema filters which are stored
- * inAppSearch.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
*/
@NonNull
public ResultSpecProto toResultSpecProto(
@NonNull Map<String, Set<String>> namespaceMap,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ @NonNull SchemaCache schemaCache) {
ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
.setNumPerPage(mSearchSpec.getResultCountPerPage())
.setSnippetSpec(
@@ -429,7 +445,7 @@
break;
case SearchSpec.GROUPING_TYPE_PER_SCHEMA:
addPerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
- mSearchSpec.getResultGroupingLimit(), schemaMap, resultSpecBuilder);
+ mSearchSpec.getResultGroupingLimit(), schemaCache, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
break;
case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE:
@@ -441,20 +457,20 @@
case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
addPerPackagePerSchemaResultGroupings(mCurrentSearchSpecPrefixFilters,
mSearchSpec.getResultGroupingLimit(),
- schemaMap, resultSpecBuilder);
+ schemaCache, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
break;
case SearchSpec.GROUPING_TYPE_PER_NAMESPACE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
addPerNamespaceAndSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
mSearchSpec.getResultGroupingLimit(),
- namespaceMap, schemaMap, resultSpecBuilder);
+ namespaceMap, schemaCache, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
break;
case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE
| SearchSpec.GROUPING_TYPE_PER_SCHEMA:
addPerPackagePerNamespacePerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
mSearchSpec.getResultGroupingLimit(),
- namespaceMap, schemaMap, resultSpecBuilder);
+ namespaceMap, schemaCache, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
break;
default:
@@ -468,7 +484,7 @@
// Rewrite filters to include a database prefix.
for (int i = 0; i < typePropertyMaskBuilders.size(); i++) {
String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();
- if (unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD)) {
+ if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build());
} else {
// Qualify the given schema types
@@ -502,6 +518,8 @@
addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder);
protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression());
+ protoBuilder.addAllAdditionalAdvancedScoringExpressions(
+ mSearchSpec.getInformationalRankingExpressions());
return protoBuilder.build();
}
@@ -536,6 +554,26 @@
}
/**
+ * Maps a list of AppSearch search feature strings to the list of the corresponding Icing
+ * feature strings.
+ *
+ * @param appSearchFeatures The list of AppSearch search feature strings.
+ */
+ @NonNull
+ private static List<String> toIcingSearchFeatures(@NonNull List<String> appSearchFeatures) {
+ List<String> result = new ArrayList<>();
+ for (int i = 0; i < appSearchFeatures.size(); i++) {
+ String appSearchFeature = appSearchFeatures.get(i);
+ if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) {
+ result.add("HAS_PROPERTY_FUNCTION");
+ } else {
+ result.add(appSearchFeature);
+ }
+ }
+ return result;
+ }
+
+ /**
* Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the
* same as the list of namespaces. If a namespace exists under different packages and/or
* different databases, they should still be grouped together.
@@ -593,7 +631,7 @@
String packageName = getPackageName(prefix);
// Create a new prefix without the database name. This will allow us to group namespaces
// that have the same name and package but a different database name together.
- String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+ String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */"");
for (String prefixedNamespace : prefixedNamespaces) {
String namespace;
try {
@@ -623,17 +661,15 @@
* different databases, they should still be grouped together.
*
* @param prefixes Prefixes that we should prepend to all our filters.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
*/
private static Map<String, List<String>> getSchemaToPrefixedSchemas(
@NonNull Set<String> prefixes,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ @NonNull SchemaCache schemaCache) {
Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>();
for (String prefix : prefixes) {
- Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
- if (prefixedSchemas == null) {
- continue;
- }
+ Map<String, SchemaTypeConfigProto> prefixedSchemas =
+ schemaCache.getSchemaMapForPrefix(prefix);
for (String prefixedSchema : prefixedSchemas.keySet()) {
String schema;
try {
@@ -661,17 +697,15 @@
* schema, then those should be grouped together.
*
* @param prefixes Prefixes that we should prepend to all our filters.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
*/
private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas(
@NonNull Set<String> prefixes,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ @NonNull SchemaCache schemaCache) {
Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>();
for (String prefix : prefixes) {
- Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
- if (prefixedSchemas == null) {
- continue;
- }
+ Map<String, SchemaTypeConfigProto> prefixedSchemas =
+ schemaCache.getSchemaMapForPrefix(prefix);
String packageName = getPackageName(prefix);
// Create a new prefix without the database name. This will allow us to group schemas
// that have the same name and package but a different database name together.
@@ -733,16 +767,16 @@
*
* @param prefixes Prefixes that we should prepend to all our filters.
* @param maxNumResults The maximum number of results for each grouping to support.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @param resultSpecBuilder ResultSpecs as a specified by client.
*/
private static void addPerPackagePerSchemaResultGroupings(
@NonNull Set<String> prefixes,
int maxNumResults,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull ResultSpecProto.Builder resultSpecBuilder) {
Map<String, List<String>> packageAndSchemaToSchemas =
- getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+ getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -764,19 +798,19 @@
* @param prefixes Prefixes that we should prepend to all our filters.
* @param maxNumResults The maximum number of results for each grouping to support.
* @param namespaceMap The namespace map contains all prefixed existing namespaces.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @param resultSpecBuilder ResultSpec as specified by client.
*/
private static void addPerPackagePerNamespacePerSchemaResultGrouping(
@NonNull Set<String> prefixes,
int maxNumResults,
@NonNull Map<String, Set<String>> namespaceMap,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull ResultSpecProto.Builder resultSpecBuilder) {
Map<String, List<String>> packageAndNamespaceToNamespaces =
getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
Map<String, List<String>> packageAndSchemaToSchemas =
- getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+ getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
@@ -884,16 +918,16 @@
*
* @param prefixes Prefixes that we should prepend to all our filters.
* @param maxNumResults The maximum number of results for each grouping to support.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @param resultSpecBuilder ResultSpec as specified by client.
*/
private static void addPerSchemaResultGrouping(
@NonNull Set<String> prefixes,
int maxNumResults,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull ResultSpecProto.Builder resultSpecBuilder) {
Map<String, List<String>> schemaToPrefixedSchemas =
- getSchemaToPrefixedSchemas(prefixes, schemaMap);
+ getSchemaToPrefixedSchemas(prefixes, schemaCache);
for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -915,19 +949,19 @@
* @param prefixes Prefixes that we should prepend to all our filters.
* @param maxNumResults The maximum number of results for each grouping to support.
* @param namespaceMap The namespace map contains all prefixed existing namespaces.
- * @param schemaMap The schema map contains all prefixed existing schema types.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @param resultSpecBuilder ResultSpec as specified by client.
*/
private static void addPerNamespaceAndSchemaResultGrouping(
@NonNull Set<String> prefixes,
int maxNumResults,
@NonNull Map<String, Set<String>> namespaceMap,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull ResultSpecProto.Builder resultSpecBuilder) {
Map<String, List<String>> namespaceToPrefixedNamespaces =
getNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
Map<String, List<String>> schemaToPrefixedSchemas =
- getSchemaToPrefixedSchemas(prefixes, schemaMap);
+ getSchemaToPrefixedSchemas(prefixes, schemaCache);
for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
index 9015102..d3b7687 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.collection.ArraySet;
import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -74,33 +75,26 @@
* intersection set with those prefixed schema candidates that are stored in AppSearch.
*
* @param prefixes Set of database prefix which the caller want to access.
- * @param schemaMap The cached Map of
- * {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
- * stores all prefixed schema filters which are stored in
- * AppSearch.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
* @param inputSchemaFilters The set contains all desired but un-prefixed namespace filters
* of user. If the inputSchemaFilters is empty, all existing
* prefixedCandidates will be added to the prefixedTargetFilters.
*/
static Set<String> generateTargetSchemaFilters(
@NonNull Set<String> prefixes,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull SchemaCache schemaCache,
@NonNull List<String> inputSchemaFilters) {
Set<String> targetPrefixedSchemaFilters = new ArraySet<>();
// Append prefix to input schema filters and get the intersection of existing schema filter.
for (String prefix : prefixes) {
// Step1: find all prefixed schema candidates that are stored in AppSearch.
- Map<String, SchemaTypeConfigProto> prefixedSchemaMap = schemaMap.get(prefix);
- if (prefixedSchemaMap == null) {
- // This is should never happen. All prefixes should be verified before reach
- // here.
- continue;
- }
+ Map<String, SchemaTypeConfigProto> prefixedSchemaMap =
+ schemaCache.getSchemaMapForPrefix(prefix);
Set<String> prefixedSchemaCandidates = prefixedSchemaMap.keySet();
- // Step2: get the intersection of user searching filters and those candidates which are
- // stored in AppSearch.
- addIntersectedFilters(prefix, prefixedSchemaCandidates, inputSchemaFilters,
- targetPrefixedSchemaFilters);
+ // Step2: get the intersection of user searching filters (after polymorphism
+ // expansion) and those candidates which are stored in AppSearch.
+ addIntersectedPolymorphicSchemaFilters(prefix, prefixedSchemaCandidates,
+ schemaCache, inputSchemaFilters, targetPrefixedSchemaFilters);
}
return targetPrefixedSchemaFilters;
}
@@ -136,4 +130,45 @@
}
}
}
+
+ /**
+ * Find the schema intersection set of candidates existing in AppSearch and user specified
+ * schema filters after polymorphism expansion.
+ *
+ * @param prefix The package and database's identifier.
+ * @param prefixedCandidates The set contains all prefixed candidates which are existing
+ * in a database.
+ * @param schemaCache The SchemaCache instance held in AppSearch.
+ * @param inputFilters The set contains all desired but un-prefixed filters of user.
+ * If the inputFilters is empty, all prefixedCandidates will be
+ * added to the prefixedTargetFilters.
+ * @param prefixedTargetFilters The output set contains all desired prefixed filters which
+ * are existing in the database.
+ */
+ private static void addIntersectedPolymorphicSchemaFilters(
+ @NonNull String prefix,
+ @NonNull Set<String> prefixedCandidates,
+ @NonNull SchemaCache schemaCache,
+ @NonNull List<String> inputFilters,
+ @NonNull Set<String> prefixedTargetFilters) {
+ if (inputFilters.isEmpty()) {
+ // Client didn't specify certain schemas to search over, add all candidates.
+ // Polymorphism expansion is not necessary here, since expanding the set of all
+ // schema types will result in the same set of schema types.
+ prefixedTargetFilters.addAll(prefixedCandidates);
+ return;
+ }
+
+ Set<String> currentPrefixedTargetFilters = new ArraySet<>();
+ for (int i = 0; i < inputFilters.size(); i++) {
+ String prefixedTargetSchemaFilter = prefix + inputFilters.get(i);
+ if (prefixedCandidates.contains(prefixedTargetSchemaFilter)) {
+ currentPrefixedTargetFilters.add(prefixedTargetSchemaFilter);
+ }
+ }
+ // Expand schema filters by polymorphism.
+ currentPrefixedTargetFilters = schemaCache.getSchemaTypesWithDescendants(prefix,
+ currentPrefixedTargetFilters);
+ prefixedTargetFilters.addAll(currentPrefixedTargetFilters);
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
index 52f57bd..7c99bb2 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
@@ -19,10 +19,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.localstorage.SchemaCache;
import androidx.core.util.Preconditions;
import com.google.android.icing.proto.NamespaceDocumentUriGroup;
-import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.SuggestionScoringSpecProto;
import com.google.android.icing.proto.SuggestionSpecProto;
import com.google.android.icing.proto.TermMatchType;
@@ -73,7 +73,7 @@
@NonNull SearchSuggestionSpec searchSuggestionSpec,
@NonNull Set<String> prefixes,
@NonNull Map<String, Set<String>> namespaceMap,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ @NonNull SchemaCache schemaCache) {
mSuggestionQueryExpression = Preconditions.checkNotNull(suggestionQueryExpression);
mSearchSuggestionSpec = Preconditions.checkNotNull(searchSuggestionSpec);
mPrefixes = Preconditions.checkNotNull(prefixes);
@@ -83,11 +83,11 @@
prefixes, namespaceMap, searchSuggestionSpec.getFilterNamespaces());
mTargetPrefixedSchemaFilters =
SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
- prefixes, schemaMap, searchSuggestionSpec.getFilterSchemas());
+ prefixes, schemaCache, searchSuggestionSpec.getFilterSchemas());
}
/**
- * @return whether this search's target filters are empty. If any target filter is empty, we
+ * Returns whether this search's target filters are empty. If any target filter is empty, we
* should skip send request to Icing.
*/
public boolean hasNothingToSearch() {
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 02d2971..5f28ea0 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
@@ -45,6 +45,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class CallStats {
+ /** Call types. */
@IntDef(value = {
CALL_TYPE_UNKNOWN,
CALL_TYPE_INITIALIZE,
@@ -77,6 +78,7 @@
CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+ CALL_TYPE_EXECUTE_APP_FUNCTION
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallType {
@@ -113,6 +115,7 @@
public static final int CALL_TYPE_REGISTER_OBSERVER_CALLBACK = 28;
public static final int CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK = 29;
public static final int CALL_TYPE_GLOBAL_GET_NEXT_PAGE = 30;
+ public static final int CALL_TYPE_EXECUTE_APP_FUNCTION = 31;
// These strings are for the subset of call types that correspond to an AppSearchManager API
private static final String CALL_TYPE_STRING_INITIALIZE = "initialize";
@@ -144,6 +147,7 @@
private static final String CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK =
"globalUnregisterObserverCallback";
private static final String CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE = "globalGetNextPage";
+ private static final String CALL_TYPE_STRING_EXECUTE_APP_FUNCTION = "executeAppFunction";
@Nullable
private final String mPackageName;
@@ -410,6 +414,8 @@
return CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK;
case CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE:
return CALL_TYPE_GLOBAL_GET_NEXT_PAGE;
+ case CALL_TYPE_STRING_EXECUTE_APP_FUNCTION:
+ return CALL_TYPE_EXECUTE_APP_FUNCTION;
default:
return CALL_TYPE_UNKNOWN;
}
@@ -445,6 +451,7 @@
CALL_TYPE_GET_STORAGE_INFO,
CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
- CALL_TYPE_GLOBAL_GET_NEXT_PAGE));
+ CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+ CALL_TYPE_EXECUTE_APP_FUNCTION));
}
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
new file mode 100644
index 0000000..4477ce3
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 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.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a click action, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ClickStats {
+ private final long mTimestampMillis;
+
+ private final long mTimeStayOnResultMillis;
+
+ private final int mResultRankInBlock;
+
+ private final int mResultRankGlobal;
+
+ private final boolean mIsGoodClick;
+
+ ClickStats(@NonNull Builder builder) {
+ Preconditions.checkNotNull(builder);
+ mTimestampMillis = builder.mTimestampMillis;
+ mTimeStayOnResultMillis = builder.mTimeStayOnResultMillis;
+ mResultRankInBlock = builder.mResultRankInBlock;
+ mResultRankGlobal = builder.mResultRankGlobal;
+ mIsGoodClick = builder.mIsGoodClick;
+ }
+
+ /** Returns the click action timestamp in milliseconds since Unix epoch. */
+ public long getTimestampMillis() {
+ return mTimestampMillis;
+ }
+
+ /** Returns the time (duration) of the user staying on the clicked result. */
+ public long getTimeStayOnResultMillis() {
+ return mTimeStayOnResultMillis;
+ }
+
+ /** Returns the in-block rank of the clicked result. */
+ public int getResultRankInBlock() {
+ return mResultRankInBlock;
+ }
+
+ /** Returns the global rank of the clicked result. */
+ public int getResultRankGlobal() {
+ return mResultRankGlobal;
+ }
+
+ /**
+ * Returns whether this click is a good click or not.
+ *
+ * @see Builder#setIsGoodClick
+ */
+ public boolean isGoodClick() {
+ return mIsGoodClick;
+ }
+
+ /** Builder for {@link ClickStats} */
+ public static final class Builder {
+ private long mTimestampMillis;
+
+ private long mTimeStayOnResultMillis;
+
+ private int mResultRankInBlock;
+
+ private int mResultRankGlobal;
+
+ private boolean mIsGoodClick = true;
+
+ /** Sets the click action timestamp in milliseconds since Unix epoch. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTimestampMillis(long timestampMillis) {
+ mTimestampMillis = timestampMillis;
+ return this;
+ }
+
+ /** Sets the time (duration) of the user staying on the clicked result. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+ mTimeStayOnResultMillis = timeStayOnResultMillis;
+ return this;
+ }
+
+ /** Sets the in-block rank of the clicked result. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankInBlock(int resultRankInBlock) {
+ mResultRankInBlock = resultRankInBlock;
+ return this;
+ }
+
+ /** Sets the global rank of the clicked result. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankGlobal(int resultRankGlobal) {
+ mResultRankGlobal = resultRankGlobal;
+ return this;
+ }
+
+ /**
+ * Sets the flag indicating whether the click is good or not.
+ *
+ * <p>A good click means the user is satisfied by the clicked document. The caller should
+ * define its own criteria and set this field accordingly.
+ *
+ * <p>The default value is true if unset. We should treat it as a good click by default if
+ * the caller didn't specify or could not determine for several reasons:
+ *
+ * <ul>
+ * <li>It may be difficult for the caller to determine if the user is satisfied by the
+ * clicked document or not.
+ * <li>AppSearch collects search quality metrics that are related to number of good
+ * clicks. We don't want to demote the quality score aggressively by the undetermined
+ * ones.
+ * </ul>
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setIsGoodClick(boolean isGoodClick) {
+ mIsGoodClick = isGoodClick;
+ return this;
+ }
+
+ /** Builds a new {@link ClickStats} from the {@link ClickStats.Builder}. */
+ @NonNull
+ public ClickStats build() {
+ return new ClickStats(/* builder= */ this);
+ }
+ }
+}
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 73a8c96..d650e7d 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
@@ -37,6 +37,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class RemoveStats {
+ /** Types of stats available for remove API. */
@IntDef(value = {
// It needs to be sync with DeleteType.Code in
// external/icing/proto/icing/proto/logging.proto#DeleteStatsProto
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
new file mode 100644
index 0000000..0886d2f
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2024 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.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+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;
+import java.util.List;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search intent, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * A search intent includes a valid AppSearch search request, potentially followed by several user
+ * click actions (see {@link ClickStats}) on fetched result documents. Related information of a
+ * search intent will be extracted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchIntentStats {
+ /** AppSearch query correction type compared with the previous query. */
+ @IntDef(value = {
+ QUERY_CORRECTION_TYPE_UNKNOWN,
+ QUERY_CORRECTION_TYPE_FIRST_QUERY,
+ QUERY_CORRECTION_TYPE_REFINEMENT,
+ QUERY_CORRECTION_TYPE_ABANDONMENT,
+ QUERY_CORRECTION_TYPE_END_SESSION,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface QueryCorrectionType {
+ }
+
+ public static final int QUERY_CORRECTION_TYPE_UNKNOWN = 0;
+
+ public static final int QUERY_CORRECTION_TYPE_FIRST_QUERY = 1;
+
+ public static final int QUERY_CORRECTION_TYPE_REFINEMENT = 2;
+
+ public static final int QUERY_CORRECTION_TYPE_ABANDONMENT = 3;
+
+ public static final int QUERY_CORRECTION_TYPE_END_SESSION = 4;
+
+ @NonNull
+ private final String mPackageName;
+
+ @Nullable
+ private final String mDatabase;
+
+ @Nullable
+ private final String mPrevQuery;
+
+ @Nullable
+ private final String mCurrQuery;
+
+ private final long mTimestampMillis;
+
+ private final int mNumResultsFetched;
+
+ @QueryCorrectionType
+ private final int mQueryCorrectionType;
+
+ @NonNull
+ private final List<ClickStats> mClicksStats;
+
+ SearchIntentStats(@NonNull Builder builder) {
+ Preconditions.checkNotNull(builder);
+ mPackageName = builder.mPackageName;
+ mDatabase = builder.mDatabase;
+ mPrevQuery = builder.mPrevQuery;
+ mCurrQuery = builder.mCurrQuery;
+ mTimestampMillis = builder.mTimestampMillis;
+ mNumResultsFetched = builder.mNumResultsFetched;
+ mQueryCorrectionType = builder.mQueryCorrectionType;
+ mClicksStats = builder.mClicksStats;
+ }
+
+ /** Returns calling package name. */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns calling database name.
+ *
+ * <p>For global search, database name will be null.
+ */
+ @Nullable
+ public String getDatabase() {
+ return mDatabase;
+ }
+
+ /** Returns the raw query string of the previous search intent. */
+ @Nullable
+ public String getPrevQuery() {
+ return mPrevQuery;
+ }
+
+ /** Returns the raw query string of this (current) search intent. */
+ @Nullable
+ public String getCurrQuery() {
+ return mCurrQuery;
+ }
+
+ /** Returns the search intent timestamp in milliseconds since Unix epoch. */
+ public long getTimestampMillis() {
+ return mTimestampMillis;
+ }
+
+ /**
+ * Returns total number of results fetched from AppSearch by the client in this search intent.
+ */
+ public int getNumResultsFetched() {
+ return mNumResultsFetched;
+ }
+
+ /**
+ * Returns the correction type of the query in this search intent compared with the previous
+ * search intent. Default value: {@link SearchIntentStats#QUERY_CORRECTION_TYPE_UNKNOWN}.
+ */
+ @QueryCorrectionType
+ public int getQueryCorrectionType() {
+ return mQueryCorrectionType;
+ }
+
+ /** Returns the list of {@link ClickStats} in this search intent. */
+ @NonNull
+ public List<ClickStats> getClicksStats() {
+ return mClicksStats;
+ }
+
+ /** Builder for {@link SearchIntentStats} */
+ public static final class Builder {
+ @NonNull
+ private final String mPackageName;
+
+ @Nullable
+ private String mDatabase;
+
+ @Nullable
+ private String mPrevQuery;
+
+ @Nullable
+ private String mCurrQuery;
+
+ private long mTimestampMillis;
+
+ private int mNumResultsFetched;
+
+ @QueryCorrectionType
+ private int mQueryCorrectionType = QUERY_CORRECTION_TYPE_UNKNOWN;
+
+ @NonNull
+ private List<ClickStats> mClicksStats = new ArrayList<>();
+
+ private boolean mBuilt = false;
+
+ /** Constructor for the {@link Builder}. */
+ public Builder(@NonNull String packageName) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ }
+
+ /** Constructor the {@link Builder} from an existing {@link SearchIntentStats}. */
+ public Builder(@NonNull SearchIntentStats searchIntentStats) {
+ Preconditions.checkNotNull(searchIntentStats);
+
+ mPackageName = searchIntentStats.getPackageName();
+ mDatabase = searchIntentStats.getDatabase();
+ mPrevQuery = searchIntentStats.getPrevQuery();
+ mCurrQuery = searchIntentStats.getCurrQuery();
+ mTimestampMillis = searchIntentStats.getTimestampMillis();
+ mNumResultsFetched = searchIntentStats.getNumResultsFetched();
+ mQueryCorrectionType = searchIntentStats.getQueryCorrectionType();
+ mClicksStats.addAll(searchIntentStats.getClicksStats());
+ }
+
+ /**
+ * Sets calling database name.
+ *
+ * <p>For global search, database name will be null.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setDatabase(@Nullable String database) {
+ resetIfBuilt();
+ mDatabase = database;
+ return this;
+ }
+
+ /** Sets the raw query string of the previous search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setPrevQuery(@Nullable String prevQuery) {
+ resetIfBuilt();
+ mPrevQuery = prevQuery;
+ return this;
+ }
+
+ /** Sets the raw query string of this (current) search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setCurrQuery(@Nullable String currQuery) {
+ resetIfBuilt();
+ mCurrQuery = currQuery;
+ return this;
+ }
+
+ /** Sets the search intent timestamp in milliseconds since Unix epoch. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTimestampMillis(long timestampMillis) {
+ resetIfBuilt();
+ mTimestampMillis = timestampMillis;
+ return this;
+ }
+
+ /**
+ * Sets total number of results fetched from AppSearch by the client in this search intent.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setNumResultsFetched(int numResultsFetched) {
+ resetIfBuilt();
+ mNumResultsFetched = numResultsFetched;
+ return this;
+ }
+
+ /**
+ * Sets the correction type of the query in this search intent compared with the previous
+ * search intent.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQueryCorrectionType(@QueryCorrectionType int queryCorrectionType) {
+ resetIfBuilt();
+ mQueryCorrectionType = queryCorrectionType;
+ return this;
+ }
+
+ /** Adds one or more {@link ClickStats} objects to this search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addClicksStats(@NonNull ClickStats... clicksStats) {
+ Preconditions.checkNotNull(clicksStats);
+ resetIfBuilt();
+ return addClicksStats(Arrays.asList(clicksStats));
+ }
+
+ /** Adds a collection of {@link ClickStats} objects to this search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addClicksStats(@NonNull Collection<? extends ClickStats> clicksStats) {
+ Preconditions.checkNotNull(clicksStats);
+ resetIfBuilt();
+ mClicksStats.addAll(clicksStats);
+ return this;
+ }
+
+ /**
+ * If built, make a copy of previous data for every field so that the builder can be reused.
+ */
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mClicksStats = new ArrayList<>(mClicksStats);
+ mBuilt = false;
+ }
+ }
+
+ /** Builds a new {@link SearchIntentStats} from the {@link Builder}. */
+ @NonNull
+ public SearchIntentStats build() {
+ mBuilt = true;
+ return new SearchIntentStats(/* builder= */ this);
+ }
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java
new file mode 100644
index 0000000..11d8ea3
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 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.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search session, converted from {@link
+ * androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}. It contains a list of
+ * {@link SearchIntentStats} and aggregated metrics of them.
+ *
+ * <p>A search session is consist of a sequence of related search intents. See {@link
+ * SearchIntentStats} for more details.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchSessionStats {
+ @NonNull private final String mPackageName;
+
+ @Nullable private final String mDatabase;
+
+ @NonNull private final List<SearchIntentStats> mSearchIntentsStats;
+
+ SearchSessionStats(@NonNull Builder builder) {
+ Preconditions.checkNotNull(builder);
+ mPackageName = builder.mPackageName;
+ mDatabase = builder.mDatabase;
+ mSearchIntentsStats = builder.mSearchIntentsStats;
+ }
+
+ /**
+ * Returns a nullable {@link SearchIntentStats} instance containing information of the last
+ * search intent which ended the search session.
+ *
+ * <p>If {@link #getSearchIntentsStats} is empty (i.e. the caller didn't add any {@link
+ * SearchIntentStats} via {@link Builder#addSearchIntentsStats}), then return null.
+ *
+ * <p>It is similar to the last element in {@link #getSearchIntentsStats}, except there is no
+ * previous query and the query correction type is tagged as {@link
+ * SearchIntentStats#QUERY_CORRECTION_TYPE_END_SESSION}.
+ *
+ * <p>This stats is useful to determine whether the user ended the search session with
+ * satisfaction (i.e. had found desired result documents) or not.
+ */
+ @Nullable
+ public SearchIntentStats getEndSessionSearchIntentStats() {
+ if (mSearchIntentsStats.isEmpty()) {
+ return null;
+ }
+
+ SearchIntentStats lastSearchIntentStats =
+ mSearchIntentsStats.get(mSearchIntentsStats.size() - 1);
+ return new SearchIntentStats.Builder(lastSearchIntentStats)
+ .setPrevQuery(null)
+ .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION)
+ .build();
+ }
+
+ /** Returns calling package name. */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns calling database name.
+ *
+ * <p>For global search, database name will be null.
+ */
+ @Nullable
+ public String getDatabase() {
+ return mDatabase;
+ }
+
+ /** Returns the list of {@link SearchIntentStats} in this search session. */
+ @NonNull
+ public List<SearchIntentStats> getSearchIntentsStats() {
+ return mSearchIntentsStats;
+ }
+
+ /** Builder for {@link SearchSessionStats}. */
+ public static final class Builder {
+ @NonNull private final String mPackageName;
+
+ @Nullable private String mDatabase;
+
+ @NonNull private List<SearchIntentStats> mSearchIntentsStats = new ArrayList<>();
+
+ private boolean mBuilt = false;
+
+ /** Constructor for the {@link Builder}. */
+ public Builder(@NonNull String packageName) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ }
+
+ /**
+ * Sets calling database name.
+ *
+ * <p>For global search, database name will be null.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setDatabase(@Nullable String database) {
+ resetIfBuilt();
+ mDatabase = database;
+ return this;
+ }
+
+ /** Adds one or more {@link SearchIntentStats} objects to this search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addSearchIntentsStats(@NonNull SearchIntentStats... searchIntentsStats) {
+ Preconditions.checkNotNull(searchIntentsStats);
+ resetIfBuilt();
+ return addSearchIntentsStats(Arrays.asList(searchIntentsStats));
+ }
+
+ /** Adds a collection of {@link SearchIntentStats} objects to this search intent. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addSearchIntentsStats(
+ @NonNull Collection<? extends SearchIntentStats> searchIntentsStats) {
+ Preconditions.checkNotNull(searchIntentsStats);
+ resetIfBuilt();
+ mSearchIntentsStats.addAll(searchIntentsStats);
+ return this;
+ }
+
+ /**
+ * If built, make a copy of previous data for every field so that the builder can be reused.
+ */
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mSearchIntentsStats = new ArrayList<>(mSearchIntentsStats);
+ mBuilt = false;
+ }
+ }
+
+ /** Builds a new {@link SearchSessionStats} from the {@link Builder}. */
+ @NonNull
+ public SearchSessionStats build() {
+ mBuilt = true;
+ return new SearchSessionStats(/* builder= */ this);
+ }
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
index a87ae04..28d5117 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -36,6 +36,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SearchStats {
+ /** Types of Visibility scopes available for search. */
@IntDef(value = {
// Searches apps' own documents.
VISIBILITY_SCOPE_LOCAL,
@@ -137,6 +138,7 @@
private final int mNativeNumJoinedResultsCurrentPage;
/** Time taken to join documents together. */
private final int mNativeJoinLatencyMillis;
+ @Nullable private final String mSearchSourceLogTag;
SearchStats(@NonNull Builder builder) {
Preconditions.checkNotNull(builder);
@@ -170,6 +172,7 @@
mJoinType = builder.mJoinType;
mNativeNumJoinedResultsCurrentPage = builder.mNativeNumJoinedResultsCurrentPage;
mNativeJoinLatencyMillis = builder.mNativeJoinLatencyMillis;
+ mSearchSourceLogTag = builder.mSearchSourceLogTag;
}
/** Returns the package name of the session. */
@@ -342,6 +345,12 @@
return mNativeJoinLatencyMillis;
}
+ /** Returns a tag to indicate the source of this search, or {code null} if never set. */
+ @Nullable
+ public String getSearchSourceLogTag() {
+ return mSearchSourceLogTag;
+ }
+
/** Builder for {@link SearchStats} */
public static class Builder {
@NonNull
@@ -377,6 +386,7 @@
@JoinableValueType int mJoinType;
int mNativeNumJoinedResultsCurrentPage;
int mNativeJoinLatencyMillis;
+ @Nullable String mSearchSourceLogTag;
/**
* Constructor
@@ -604,6 +614,7 @@
}
/** Sets whether or not this is a join query */
+ @CanIgnoreReturnValue
@NonNull
public Builder setJoinType(@JoinableValueType int joinType) {
mJoinType = joinType;
@@ -611,6 +622,7 @@
}
/** Set the total number of joined documents in a page. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeNumJoinedResultsCurrentPage(int nativeNumJoinedResultsCurrentPage) {
mNativeNumJoinedResultsCurrentPage = nativeNumJoinedResultsCurrentPage;
@@ -618,12 +630,21 @@
}
/** Sets time it takes to join documents together in icing. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeJoinLatencyMillis(int nativeJoinLatencyMillis) {
mNativeJoinLatencyMillis = nativeJoinLatencyMillis;
return this;
}
+ /** Sets a tag to indicate the source of this search. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSearchSourceLogTag(@Nullable String searchSourceLogTag) {
+ mSearchSourceLogTag = searchSourceLogTag;
+ return this;
+ }
+
/**
* Constructs a new {@link SearchStats} from the contents of this
* {@link SearchStats.Builder}.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
new file mode 100644
index 0000000..baa3e4e
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ * <!--@exportToFramework:ifJetpack()-->
+ * {@link androidx.appsearch.usagereporting.ClickAction}
+ * <!--@exportToFramework:else()
+ * click action
+ * -->
+ * {@link GenericDocument}, which contains getters for click action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ClickActionGenericDocument extends TakenActionGenericDocument {
+ private static final String PROPERTY_PATH_QUERY = "query";
+ private static final String PROPERTY_PATH_RESULT_RANK_IN_BLOCK = "resultRankInBlock";
+ private static final String PROPERTY_PATH_RESULT_RANK_GLOBAL = "resultRankGlobal";
+ private static final String PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS = "timeStayOnResultMillis";
+
+ ClickActionGenericDocument(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+ }
+
+ /** Returns the string value of property {@code query}. */
+ @Nullable
+ public String getQuery() {
+ return getPropertyString(PROPERTY_PATH_QUERY);
+ }
+
+ /** Returns the integer value of property {@code resultRankInBlock}. */
+ public int getResultRankInBlock() {
+ return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK);
+ }
+
+ /** Returns the integer value of property {@code resultRankGlobal}. */
+ public int getResultRankGlobal() {
+ return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL);
+ }
+
+ /** Returns the long value of property {@code timeStayOnResultMillis}. */
+ public long getTimeStayOnResultMillis() {
+ return getPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS);
+ }
+
+ /** Builder for {@link ClickActionGenericDocument}. */
+ public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+ /**
+ * Creates a new {@link ClickActionGenericDocument.Builder}.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ *
+ * @param namespace the namespace to set for the {@link GenericDocument}.
+ * @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#setSchemaAsync} prior
+ * to inserting a document of this {@code schemaType} into the
+ * AppSearch index using
+ * {@link AppSearchSession#putAsync}.
+ * Otherwise, the document will be rejected by
+ * {@link AppSearchSession#putAsync} with result code
+ * {@link AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+ Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_CLICK);
+ }
+
+ /**
+ * Creates a new {@link ClickActionGenericDocument.Builder} from an existing
+ * {@link GenericDocument}.
+ *
+ * @param document a generic document object.
+ *
+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+ * not {@link ActionConstants#ACTION_TYPE_CLICK}.
+ */
+ public Builder(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+
+ if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+ != ActionConstants.ACTION_TYPE_CLICK) {
+ throw new IllegalArgumentException(
+ "Invalid action type for ClickActionGenericDocument");
+ }
+ }
+
+ /**
+ * Sets the string value of property {@code query} by the user-entered search input
+ * (without any operators or rewriting) that yielded the
+ * {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQuery(@NonNull String query) {
+ Preconditions.checkNotNull(query);
+ setPropertyString(PROPERTY_PATH_QUERY, query);
+ return this;
+ }
+
+ /**
+ * Sets the integer value of property {@code resultRankInBlock} by the rank of the clicked
+ * {@link androidx.appsearch.app.SearchResult} document among the user-defined block.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankInBlock(int resultRankInBlock) {
+ Preconditions.checkArgumentNonnegative(resultRankInBlock);
+ setPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK, resultRankInBlock);
+ return this;
+ }
+
+ /**
+ * Sets the integer value of property {@code resultRankGlobal} by the global rank of the
+ * clicked {@link androidx.appsearch.app.SearchResult} document.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankGlobal(int resultRankGlobal) {
+ Preconditions.checkArgumentNonnegative(resultRankGlobal);
+ setPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL, resultRankGlobal);
+ return this;
+ }
+
+ /**
+ * Sets the integer value of property {@code timeStayOnResultMillis} by the time in
+ * milliseconds that user stays on the {@link androidx.appsearch.app.SearchResult} document
+ * after clicking it.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+ setPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS, timeStayOnResultMillis);
+ return this;
+ }
+
+ /** Builds a {@link ClickActionGenericDocument}. */
+ @Override
+ @NonNull
+ public ClickActionGenericDocument build() {
+ return new ClickActionGenericDocument(super.build());
+ }
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
new file mode 100644
index 0000000..bedbf21
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ * <!--@exportToFramework:ifJetpack()-->
+ * {@link androidx.appsearch.usagereporting.SearchAction}
+ * <!--@exportToFramework:else()
+ * search action
+ * -->
+ * {@link GenericDocument}, which contains getters for search action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchActionGenericDocument extends TakenActionGenericDocument {
+ private static final String PROPERTY_PATH_QUERY = "query";
+ private static final String PROPERTY_PATH_FETCHED_RESULT_COUNT = "fetchedResultCount";
+
+ SearchActionGenericDocument(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+ }
+
+ /** Returns the string value of property {@code query}. */
+ @Nullable
+ public String getQuery() {
+ return getPropertyString(PROPERTY_PATH_QUERY);
+ }
+
+ /** Returns the integer value of property {@code fetchedResultCount}. */
+ public int getFetchedResultCount() {
+ return (int) getPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT);
+ }
+
+ /** Builder for {@link SearchActionGenericDocument}. */
+ public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+ /**
+ * Creates a new {@link SearchActionGenericDocument.Builder}.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ *
+ * @param namespace the namespace to set for the {@link GenericDocument}.
+ * @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#setSchemaAsync} prior
+ * to inserting a document of this {@code schemaType} into the
+ * AppSearch index using
+ * {@link AppSearchSession#putAsync}.
+ * Otherwise, the document will be rejected by
+ * {@link AppSearchSession#putAsync} with result code
+ * {@link AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+ Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_SEARCH);
+ }
+
+ /**
+ * Creates a new {@link SearchActionGenericDocument.Builder} from an existing
+ * {@link GenericDocument}.
+ *
+ * @param document a generic document object.
+ *
+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+ * not {@link ActionConstants#ACTION_TYPE_SEARCH}.
+ */
+ public Builder(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+
+ if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+ != ActionConstants.ACTION_TYPE_SEARCH) {
+ throw new IllegalArgumentException(
+ "Invalid action type for SearchActionGenericDocument");
+ }
+ }
+
+ /**
+ * Sets the string value of property {@code query} by the user-entered search input
+ * (without any operators or rewriting).
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQuery(@NonNull String query) {
+ Preconditions.checkNotNull(query);
+ setPropertyString(PROPERTY_PATH_QUERY, query);
+ return this;
+ }
+
+ /**
+ * Sets the integer value of property {@code fetchedResultCount} by total number of results
+ * fetched from AppSearch by the client in this search action.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setFetchedResultCount(int fetchedResultCount) {
+ Preconditions.checkArgumentNonnegative(fetchedResultCount);
+ setPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT, fetchedResultCount);
+ return this;
+ }
+
+ /** Builds a {@link SearchActionGenericDocument}. */
+ @Override
+ @NonNull
+ public SearchActionGenericDocument build() {
+ return new SearchActionGenericDocument(super.build());
+ }
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java
new file mode 100644
index 0000000..72457c2
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.localstorage.stats.ClickStats;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
+import androidx.appsearch.usagereporting.ActionConstants;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Extractor class for analyzing a list of taken action {@link GenericDocument} and creating a list
+ * of {@link SearchSessionStats}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchSessionStatsExtractor {
+ // TODO(b/319285816): make thresholds configurable.
+ /**
+ * Threshold for noise search intent detection, in millisecond. A search action will be
+ * considered as a noise (and skipped) if all of the following conditions are satisfied:
+ * <ul>
+ * <li>The action timestamp (action document creation timestamp) difference between it and
+ * its previous search action is below this threshold.
+ * <li>There is no click action associated with it.
+ * <li>Its raw query string is a prefix of the previous search action's raw query string (or
+ * the other way around).
+ * </ul>
+ */
+ private static final long NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS = 2000L;
+
+ /**
+ * Threshold for independent search intent detection, in millisecond. If the action timestamp
+ * (action document creation timestamp) difference between the previous and the current search
+ * action exceeds this threshold, then the current search action will be considered as a
+ * completely independent search intent (i.e. belonging to a new search session), and there will
+ * be no correlation analysis between the previous and the current search action.
+ */
+ private static final long INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS =
+ 10L * 60 * 1000;
+
+ /**
+ * Threshold for marking good click (compared with {@code timeStayOnResultMillis}), in
+ * millisecond. A good click means the user spent decent amount of time on the clicked result
+ * document.
+ */
+ private static final long GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS = 2000L;
+
+ /**
+ * Threshold for backspace count to become query abandonment. If the user hits backspace for at
+ * least QUERY_ABANDONMENT_BACKSPACE_COUNT times, then the query correction type will be
+ * determined as abandonment.
+ */
+ private static final int QUERY_ABANDONMENT_BACKSPACE_COUNT = 2;
+
+ /**
+ * Returns the query correction type between the previous and current search actions.
+ *
+ * @param currSearchAction the current search action {@link SearchActionGenericDocument}.
+ * @param prevSearchAction the previous search action {@link SearchActionGenericDocument}.
+ */
+ public static @SearchIntentStats.QueryCorrectionType int getQueryCorrectionType(
+ @NonNull SearchActionGenericDocument currSearchAction,
+ @Nullable SearchActionGenericDocument prevSearchAction) {
+ Objects.requireNonNull(currSearchAction);
+
+ if (currSearchAction.getQuery() == null) {
+ // Query correction type cannot be determined if the client didn't provide the raw query
+ // string.
+ return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+ }
+ if (prevSearchAction == null) {
+ // If the previous search action is missing, then it is the first query.
+ return SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY;
+ } else if (prevSearchAction.getQuery() == null) {
+ // Query correction type cannot be determined if the client didn't provide the raw query
+ // string.
+ return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+ }
+
+ // Determine the query correction type by comparing the current and previous raw query
+ // strings.
+ String prevQuery = prevSearchAction.getQuery();
+ String currQuery = currSearchAction.getQuery();
+ int commonPrefixLength = getCommonPrefixLength(prevQuery, currQuery);
+ // If the user hits backspace >= QUERY_ABANDONMENT_BACKSPACE_COUNT times, then it is query
+ // abandonment. Otherwise, it is query refinement.
+ if (commonPrefixLength <= prevQuery.length() - QUERY_ABANDONMENT_BACKSPACE_COUNT) {
+ return SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+ } else {
+ return SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT;
+ }
+ }
+
+ /**
+ * Returns a list of {@link SearchSessionStats} extracted from the given list of taken action
+ * {@link GenericDocument}.
+ *
+ * <p>A search session consists of several related search intents.
+ *
+ * <p>A search intent consists of a valid search action with 0 or more click actions. To extract
+ * search intent metrics, this function will try to group the given taken actions into several
+ * search intents, and yield a {@link SearchIntentStats} for each search intent. Finally related
+ * {@link SearchIntentStats} will be wrapped into {@link SearchSessionStats}.
+ *
+ * @param packageName The package name of the caller.
+ * @param database The database name of the caller.
+ * @param genericDocuments a list of taken actions in generic document form.
+ */
+ @NonNull
+ public List<SearchSessionStats> extract(
+ @NonNull String packageName,
+ @Nullable String database,
+ @NonNull List<GenericDocument> genericDocuments) {
+ Objects.requireNonNull(genericDocuments);
+
+ // Convert GenericDocument list to TakenActionGenericDocument list and sort them by document
+ // creation timestamp.
+ List<TakenActionGenericDocument> takenActionGenericDocuments =
+ new ArrayList<>(genericDocuments.size());
+ for (int i = 0; i < genericDocuments.size(); ++i) {
+ try {
+ takenActionGenericDocuments.add(
+ TakenActionGenericDocument.create(genericDocuments.get(i)));
+ } catch (IllegalArgumentException e) {
+ // Skip generic documents with unknown action type.
+ }
+ }
+ Collections.sort(takenActionGenericDocuments,
+ (TakenActionGenericDocument doc1, TakenActionGenericDocument doc2) ->
+ Long.compare(doc1.getCreationTimestampMillis(),
+ doc2.getCreationTimestampMillis()));
+
+ List<SearchSessionStats> result = new ArrayList<>();
+ SearchSessionStats.Builder searchSessionStatsBuilder = null;
+ SearchActionGenericDocument prevSearchAction = null;
+ // Clients are expected to report search action followed by its associated click actions.
+ // For example, [searchAction1, clickAction1, searchAction2, searchAction3, clickAction2,
+ // clickAction3]:
+ // - There are 3 search actions and 3 click actions.
+ // - clickAction1 is associated with searchAction1.
+ // - There is no click action associated with searchAction2.
+ // - clickAction2 and clickAction3 are associated with searchAction3.
+ // Here we're going to break down the list into segments. Each segment starts with a search
+ // action followed by 0 or more associated click actions, and they form a single search
+ // intent. We will analyze and extract metrics from the taken actions for the search intent.
+ //
+ // If a search intent is considered independent from the previous one, then we will start a
+ // new search session analysis.
+ for (int i = 0; i < takenActionGenericDocuments.size(); ++i) {
+ if (takenActionGenericDocuments.get(i).getActionType()
+ != ActionConstants.ACTION_TYPE_SEARCH) {
+ continue;
+ }
+
+ SearchActionGenericDocument currSearchAction =
+ (SearchActionGenericDocument) takenActionGenericDocuments.get(i);
+ List<ClickActionGenericDocument> clickActions = new ArrayList<>();
+ // Get all click actions associated with the current search action by advancing until
+ // the next search action.
+ while (i + 1 < takenActionGenericDocuments.size()
+ && takenActionGenericDocuments.get(i + 1).getActionType()
+ != ActionConstants.ACTION_TYPE_SEARCH) {
+ if (takenActionGenericDocuments.get(i + 1).getActionType()
+ == ActionConstants.ACTION_TYPE_CLICK) {
+ clickActions.add(
+ (ClickActionGenericDocument) takenActionGenericDocuments.get(i + 1));
+ }
+ ++i;
+ }
+
+ // Get the reference of the next search action if it exists.
+ SearchActionGenericDocument nextSearchAction = null;
+ if (i + 1 < takenActionGenericDocuments.size()
+ && takenActionGenericDocuments.get(i + 1).getActionType()
+ == ActionConstants.ACTION_TYPE_SEARCH) {
+ nextSearchAction =
+ (SearchActionGenericDocument) takenActionGenericDocuments.get(i + 1);
+ }
+
+ if (prevSearchAction != null
+ && isIndependentSearchAction(currSearchAction, prevSearchAction)) {
+ // If the current search action is independent from the previous one, then:
+ // - Build and append the previous search session stats.
+ // - Start a new search session analysis.
+ // - Ignore the previous search action when extracting stats.
+ if (searchSessionStatsBuilder != null) {
+ result.add(searchSessionStatsBuilder.build());
+ searchSessionStatsBuilder = null;
+ }
+ prevSearchAction = null;
+ } else if (clickActions.isEmpty()
+ && isIntermediateSearchAction(
+ currSearchAction, prevSearchAction, nextSearchAction)) {
+ // If the current search action is an intermediate search action with no click
+ // actions, then we consider it as a noise and skip it.
+ continue;
+ }
+
+ // Now we get a valid search intent (the current search action + a list of click actions
+ // associated with it). Extract metrics and add SearchIntentStats into this search
+ // session.
+ if (searchSessionStatsBuilder == null) {
+ searchSessionStatsBuilder =
+ new SearchSessionStats.Builder(packageName).setDatabase(database);
+ }
+ searchSessionStatsBuilder.addSearchIntentsStats(
+ createSearchIntentStats(
+ packageName,
+ database,
+ currSearchAction,
+ clickActions,
+ prevSearchAction));
+ prevSearchAction = currSearchAction;
+ }
+ if (searchSessionStatsBuilder != null) {
+ result.add(searchSessionStatsBuilder.build());
+ }
+ return result;
+ }
+
+ /**
+ * Creates a {@link SearchIntentStats} object from the current search action + its associated
+ * click actions, and the previous search action (in generic document form).
+ */
+ private SearchIntentStats createSearchIntentStats(
+ @NonNull String packageName,
+ @Nullable String database,
+ @NonNull SearchActionGenericDocument currSearchAction,
+ @NonNull List<ClickActionGenericDocument> clickActions,
+ @Nullable SearchActionGenericDocument prevSearchAction) {
+ SearchIntentStats.Builder builder = new SearchIntentStats.Builder(packageName)
+ .setDatabase(database)
+ .setTimestampMillis(currSearchAction.getCreationTimestampMillis())
+ .setCurrQuery(currSearchAction.getQuery())
+ .setNumResultsFetched(currSearchAction.getFetchedResultCount())
+ .setQueryCorrectionType(getQueryCorrectionType(currSearchAction, prevSearchAction));
+ if (prevSearchAction != null) {
+ builder.setPrevQuery(prevSearchAction.getQuery());
+ }
+ for (int i = 0; i < clickActions.size(); ++i) {
+ builder.addClicksStats(createClickStats(clickActions.get(i)));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates a {@link ClickStats} object from the given click action (in generic document form).
+ */
+ private ClickStats createClickStats(ClickActionGenericDocument clickAction) {
+ // A click is considered good if:
+ // - The user spent decent amount of time on the clicked document.
+ // - OR the client didn't provide timeStayOnResultMillis. In this case, the value will be 0.
+ boolean isGoodClick =
+ clickAction.getTimeStayOnResultMillis() <= 0
+ || clickAction.getTimeStayOnResultMillis()
+ >= GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS;
+ return new ClickStats.Builder()
+ .setTimestampMillis(clickAction.getCreationTimestampMillis())
+ .setResultRankInBlock(clickAction.getResultRankInBlock())
+ .setResultRankGlobal(clickAction.getResultRankGlobal())
+ .setTimeStayOnResultMillis(clickAction.getTimeStayOnResultMillis())
+ .setIsGoodClick(isGoodClick)
+ .build();
+ }
+
+ /**
+ * Returns if the current search action is an intermediate search action.
+ *
+ * <p>An intermediate search action is used for detecting the situation when the user adds or
+ * deletes characters from the query (e.g. "a" -> "app" -> "apple" or "apple" -> "app" -> "a")
+ * within a short period of time. More precisely, it has to satisfy all of the following
+ * conditions:
+ * <ul>
+ * <li>There are related (non-independent) search actions before and after it.
+ * <li>It occurs within the threshold after its previous search action.
+ * <li>Its raw query string is a prefix of its previous search action's raw query string, or
+ * the opposite direction.
+ * </ul>
+ */
+ private static boolean isIntermediateSearchAction(
+ @NonNull SearchActionGenericDocument currSearchAction,
+ @Nullable SearchActionGenericDocument prevSearchAction,
+ @Nullable SearchActionGenericDocument nextSearchAction) {
+ Objects.requireNonNull(currSearchAction);
+
+ if (prevSearchAction == null || nextSearchAction == null) {
+ return false;
+ }
+
+ // Whether the next search action is independent from the current search action. If true,
+ // then the current search action will not be considered as an intermediate search action
+ // since it is the last search action of the search session.
+ boolean isNextSearchActionIndependent =
+ isIndependentSearchAction(nextSearchAction, currSearchAction);
+
+ // Whether the current search action occurs within the threshold after the previous search
+ // action.
+ boolean occursWithinTimeThreshold =
+ currSearchAction.getCreationTimestampMillis()
+ - prevSearchAction.getCreationTimestampMillis()
+ <= NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+
+ // Whether the previous search action's raw query string is a prefix of the current search
+ // action's, or the opposite direction (e.g. "app" -> "apple" and "apple" -> "app").
+ String prevQuery = prevSearchAction.getQuery();
+ String currQuery = currSearchAction.getQuery();
+ boolean isPrefix = prevQuery != null && currQuery != null
+ && (currQuery.startsWith(prevQuery) || prevQuery.startsWith(currQuery));
+
+ return !isNextSearchActionIndependent && occursWithinTimeThreshold && isPrefix;
+ }
+
+ /**
+ * Returns if the current search action is independent from the previous search action.
+ *
+ * <p>If the current search action occurs later than the threshold after the previous search
+ * action, then they are considered independent.
+ */
+ private static boolean isIndependentSearchAction(
+ @NonNull SearchActionGenericDocument currSearchAction,
+ @NonNull SearchActionGenericDocument prevSearchAction) {
+ Objects.requireNonNull(currSearchAction);
+ Objects.requireNonNull(prevSearchAction);
+
+ long searchTimeDiffMillis = currSearchAction.getCreationTimestampMillis()
+ - prevSearchAction.getCreationTimestampMillis();
+ return searchTimeDiffMillis > INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+ }
+
+ /** Returns the common prefix length of the given 2 strings. */
+ private static int getCommonPrefixLength(@NonNull String s1, @NonNull String s2) {
+ Objects.requireNonNull(s1);
+ Objects.requireNonNull(s2);
+
+ int minLength = Math.min(s1.length(), s2.length());
+ for (int i = 0; i < minLength; ++i) {
+ if (s1.charAt(i) != s2.charAt(i)) {
+ return i;
+ }
+ }
+ return minLength;
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java
new file mode 100644
index 0000000..1af156d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Abstract wrapper class for {@link GenericDocument} of all types of taken actions, which contains
+ * common getters and constants.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class TakenActionGenericDocument extends GenericDocument {
+ protected static final String PROPERTY_PATH_ACTION_TYPE = "actionType";
+
+ /**
+ * Static factory method to create a concrete object of {@link TakenActionGenericDocument} child
+ * type, according to the given {@link GenericDocument}'s action type.
+ *
+ * @param document a generic document object.
+ *
+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+ * invalid.
+ */
+ @NonNull
+ public static TakenActionGenericDocument create(@NonNull GenericDocument document)
+ throws IllegalArgumentException {
+ Preconditions.checkNotNull(document);
+ int actionType = (int) document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+ switch (actionType) {
+ case ActionConstants.ACTION_TYPE_SEARCH:
+ return new SearchActionGenericDocument.Builder(document).build();
+ case ActionConstants.ACTION_TYPE_CLICK:
+ return new ClickActionGenericDocument.Builder(document).build();
+ default:
+ throw new IllegalArgumentException(
+ "Cannot create taken action generic document with unknown action type");
+ }
+ }
+
+ protected TakenActionGenericDocument(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+ }
+
+ /** Returns the (enum) integer value of property {@code actionType}. */
+ public int getActionType() {
+ return (int) getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+ }
+
+ /** Abstract builder for {@link TakenActionGenericDocument}. */
+ abstract static class Builder<T extends Builder<T>> extends GenericDocument.Builder<T> {
+ /**
+ * Creates a new {@link TakenActionGenericDocument.Builder}.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ *
+ * @param namespace the namespace to set for the {@link GenericDocument}.
+ * @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#setSchemaAsync} prior
+ * to inserting a document of this {@code schemaType} into the
+ * AppSearch index using
+ * {@link AppSearchSession#putAsync}.
+ * Otherwise, the document will be rejected by
+ * {@link AppSearchSession#putAsync} with result code
+ * {@link AppSearchResult#RESULT_NOT_FOUND}.
+ * @param actionType the action type of the taken action. See definitions in
+ * {@link ActionConstants}.
+ */
+ Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType,
+ int actionType) {
+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+ Preconditions.checkNotNull(schemaType));
+
+ setPropertyLong(PROPERTY_PATH_ACTION_TYPE, actionType);
+ }
+
+ /**
+ * Creates a new {@link TakenActionGenericDocument.Builder} from an existing
+ * {@link GenericDocument}.
+ */
+ Builder(@NonNull GenericDocument document) {
+ super(Preconditions.checkNotNull(document));
+ }
+ }
+}
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
index a22863a..4deb5c3 100644
--- 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
@@ -44,10 +44,19 @@
return mCallingPackageName;
}
+ /** Returns whether the caller should have default access to data in its own package. */
+ public boolean doesCallerHaveSelfAccess() {
+ return true;
+ }
+
@Override
public boolean equals(@Nullable Object o) {
- if (this == o) return true;
- if (!(o instanceof CallerAccess)) return false;
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CallerAccess)) {
+ return false;
+ }
CallerAccess that = (CallerAccess) o;
return mCallingPackageName.equals(that.mCallingPackageName);
}
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
index 9db52a5..13f908d 100644
--- 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
@@ -38,4 +38,11 @@
@NonNull String packageName,
@NonNull String prefixedSchema,
@NonNull VisibilityStore visibilityStore);
+
+ /**
+ * Checks whether the given package has access to system-surfaceable schemas.
+ *
+ * @param callerPackageName Package name of the caller.
+ */
+ boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName);
}
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
index a45cf75..7bcea68 100644
--- 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
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.PackageIdentifier;
@@ -143,6 +144,7 @@
}
/** Sets whether this schema has opted out of platform surfacing. */
+ @CanIgnoreReturnValue
@NonNull
Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
@@ -150,6 +152,7 @@
}
/** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
@NonNull
Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
Preconditions.checkNotNull(packageIdentifiers);
@@ -158,6 +161,7 @@
}
/** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
@NonNull
Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
Preconditions.checkNotNull(packageIdentifier);
@@ -167,6 +171,7 @@
/** Add a set of Android role that has access to the schema this
* {@link VisibilityDocumentV1} represents. */
+ @CanIgnoreReturnValue
@NonNull
Builder setVisibleToRoles(@NonNull Set<Integer> visibleToRoles) {
Preconditions.checkNotNull(visibleToRoles);
@@ -176,6 +181,7 @@
/** Add a set of Android role that has access to the schema this
* {@link VisibilityDocumentV1} represents. */
+ @CanIgnoreReturnValue
@NonNull
Builder setVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
Preconditions.checkNotNull(visibleToPermissions);
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 e3d5916..c221673 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
@@ -27,11 +27,15 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
-import androidx.appsearch.app.VisibilityDocument;
-import androidx.appsearch.app.VisibilityPermissionDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.checker.initialization.qual.UnderInitialization;
+import androidx.appsearch.checker.initialization.qual.UnknownInitialization;
+import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchImpl;
import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.util.LogUtil;
import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;
@@ -47,10 +51,11 @@
* 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>The VisibilityStore creates a {@link InternalVisibilityConfig} for each schema. This config
+ * holds the visibility settings that apply to that schema. The VisibilityStore also creates a
+ * schema and documents for these {@link InternalVisibilityConfig} 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.
@@ -67,12 +72,13 @@
public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg";
public static final String VISIBILITY_DATABASE_NAME = "VS#Db";
+ public static final String ANDROID_V_OVERLAY_DATABASE_NAME = "VS#AndroidVDb";
/**
- * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each
+ * Map of PrefixedSchemaType to InternalVisibilityConfig stores visibility information for each
* schema type.
*/
- private final Map<String, VisibilityDocument> mVisibilityDocumentMap = new ArrayMap<>();
+ private final Map<String, InternalVisibilityConfig> mVisibilityConfigMap = new ArrayMap<>();
private final AppSearchImpl mAppSearchImpl;
@@ -86,7 +92,7 @@
new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
List<VisibilityDocumentV1> visibilityDocumentsV1s = null;
switch (getSchemaResponse.getVersion()) {
- case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:
+ case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_PACKAGE:
// TODO (b/202194495) add VisibilityDocument in version 0 back instead of using
// GenericDocument.
List<GenericDocument> visibilityDocumentsV0s =
@@ -95,7 +101,7 @@
visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0
.toVisibilityDocumentV1(visibilityDocumentsV0s);
// fall through
- case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA:
+ case VisibilityToDocumentConverter.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.
@@ -106,37 +112,12 @@
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, see comment below.
- InternalSetSchemaResponse internalSetSchemaResponse = 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);
- if (!internalSetSchemaResponse.isSuccess()) {
- // 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.
- throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
- "Fail to set the latest visibility schema to AppSearch. "
- + "You may need to update the visibility schema version "
- + "number.");
- }
- }
+ case VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST:
+ verifyOrSetLatestVisibilitySchema(getSchemaResponse);
+ // Check the version for visibility overlay database.
+ migrateVisibilityOverlayDatabase();
+ // Now we have the latest schema, load visibility config map.
+ loadVisibilityConfigMap();
break;
default:
// We must did something wrong.
@@ -146,30 +127,64 @@
}
/**
- * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous
- * {@link VisibilityDocument}s with same prefixed schema type will be overwritten.
+ * Sets visibility settings for the given {@link InternalVisibilityConfig}s. Any previous
+ * {@link InternalVisibilityConfig}s with same prefixed schema type will be overwritten.
*
- * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which
- * contains schema type's visibility information.
+ * @param prefixedVisibilityConfigs List of prefixed {@link InternalVisibilityConfig}s which
+ * contains schema type's visibility information.
* @throws AppSearchException on AppSearchImpl error.
*/
- public void setVisibility(@NonNull List<VisibilityDocument> prefixedVisibilityDocuments)
+ public void setVisibility(@NonNull List<InternalVisibilityConfig> prefixedVisibilityConfigs)
throws AppSearchException {
- Preconditions.checkNotNull(prefixedVisibilityDocuments);
+ Preconditions.checkNotNull(prefixedVisibilityConfigs);
// 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);
+ for (int i = 0; i < prefixedVisibilityConfigs.size(); i++) {
+ // put VisibilityConfig to AppSearchImpl and mVisibilityConfigMap. If there is a
+ // VisibilityConfig with same prefixed schema exists, it will be replaced by new
+ // VisibilityConfig in both AppSearch and memory look up map.
+ InternalVisibilityConfig prefixedVisibilityConfig = prefixedVisibilityConfigs.get(i);
+ InternalVisibilityConfig oldVisibilityConfig =
+ mVisibilityConfigMap.get(prefixedVisibilityConfig.getSchemaType());
mAppSearchImpl.putDocument(
VISIBILITY_PACKAGE_NAME,
VISIBILITY_DATABASE_NAME,
- prefixedVisibilityDocument.toGenericDocument(),
+ VisibilityToDocumentConverter.createVisibilityDocument(
+ prefixedVisibilityConfig),
/*sendChangeNotifications=*/ false,
/*logger=*/ null);
- mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),
- prefixedVisibilityDocument);
+
+ // Put the android V visibility overlay document to AppSearchImpl.
+ GenericDocument androidVOverlay =
+ VisibilityToDocumentConverter.createAndroidVOverlay(prefixedVisibilityConfig);
+ if (androidVOverlay != null) {
+ mAppSearchImpl.putDocument(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ androidVOverlay,
+ /*sendChangeNotifications=*/ false,
+ /*logger=*/ null);
+ } else if (isConfigContainsAndroidVOverlay(oldVisibilityConfig)) {
+ // We need to make sure to remove the VisibilityOverlay on disk as the current
+ // VisibilityConfig does not have a VisibilityOverlay.
+ // For performance improvement, we should only make the remove call if the old
+ // VisibilityConfig contains the overlay settings.
+ try {
+ mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ prefixedVisibilityConfig.getSchemaType(),
+ /*removeStatsBuilder=*/null);
+ } catch (AppSearchException e) {
+ // If it already doesn't exist, that is fine
+ if (e.getResultCode() != RESULT_NOT_FOUND) {
+ throw e;
+ }
+ }
+ }
+
+ // Put the VisibilityConfig to memory look up map.
+ mVisibilityConfigMap.put(prefixedVisibilityConfig.getSchemaType(),
+ prefixedVisibilityConfig);
}
// Now that the visibility document has been written. Persist the newly written data.
mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
@@ -182,12 +197,13 @@
public void removeVisibility(@NonNull Set<String> prefixedSchemaTypes)
throws AppSearchException {
for (String prefixedSchemaType : prefixedSchemaTypes) {
- if (mVisibilityDocumentMap.remove(prefixedSchemaType) != null) {
+ if (mVisibilityConfigMap.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,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+ prefixedSchemaType,
/*removeStatsBuilder=*/null);
} catch (AppSearchException e) {
if (e.getResultCode() == RESULT_NOT_FOUND) {
@@ -195,25 +211,45 @@
// to be fine if we cannot find it.
Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType
+ " to remove.");
- return;
+ } else {
+ throw e;
}
- throw e;
+ }
+
+ try {
+ mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ prefixedSchemaType,
+ /*removeStatsBuilder=*/null);
+ } catch (AppSearchException e) {
+ if (e.getResultCode() == RESULT_NOT_FOUND) {
+ // It's possible no overlay was set, so this this is fine.
+ if (LogUtil.DEBUG) {
+ Log.d(TAG, "Cannot find Android V overlay document for "
+ + prefixedSchemaType + " to remove.");
+ }
+ } else {
+ throw e;
+ }
}
}
}
}
- /** Gets the {@link VisibilityDocument} for the given prefixed schema type. */
+ /** Gets the {@link InternalVisibilityConfig} for the given prefixed schema type. */
@Nullable
- public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) {
- return mVisibilityDocumentMap.get(prefixedSchemaType);
+ public InternalVisibilityConfig getVisibility(@NonNull String prefixedSchemaType) {
+ return mVisibilityConfigMap.get(prefixedSchemaType);
}
/**
- * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into
- * {@link #mVisibilityDocumentMap}.
+ * Loads all stored latest {@link InternalVisibilityConfig} from Icing, and put them into
+ * {@link #mVisibilityConfigMap}.
*/
- private void loadVisibilityDocumentMap() throws AppSearchException {
+ @RequiresNonNull("mAppSearchImpl")
+ private void loadVisibilityConfigMap(@UnderInitialization VisibilityStore this)
+ throws AppSearchException {
// Populate visibility settings set
List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes();
for (int i = 0; i < cachedSchemaTypes.size(); i++) {
@@ -223,16 +259,16 @@
continue; // Our own package. Skip.
}
- VisibilityDocument visibilityDocument;
+ GenericDocument visibilityDocument;
+ GenericDocument visibilityAndroidVOverlay = null;
try {
// Note: We use the other clients' prefixed schema type as ids
- visibilityDocument = new VisibilityDocument.Builder(
- mAppSearchImpl.getDocument(
- VISIBILITY_PACKAGE_NAME,
- VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
- /*id=*/ prefixedSchemaType,
- /*typePropertyPaths=*/ Collections.emptyMap())).build();
+ visibilityDocument = mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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
@@ -242,25 +278,48 @@
// Otherwise, this is some other error we should pass up.
throw e;
}
- mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);
+
+ try {
+ visibilityAndroidVOverlay = mAppSearchImpl.getDocument(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+ /*id=*/ prefixedSchemaType,
+ /*typePropertyPaths=*/ Collections.emptyMap());
+ } catch (AppSearchException e) {
+ if (e.getResultCode() != RESULT_NOT_FOUND) {
+ // This is some other error we should pass up.
+ throw e;
+ }
+ // Otherwise we continue inserting into visibility document map as the overlay
+ // map can be null
+ }
+
+ mVisibilityConfigMap.put(
+ prefixedSchemaType,
+ VisibilityToDocumentConverter.createInternalVisibilityConfig(
+ visibilityDocument, visibilityAndroidVOverlay));
}
}
/**
- * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch.
+ * Set the latest version of {@link InternalVisibilityConfig} and its schema to AppSearch.
*/
- private void setLatestSchemaAndDocuments(@NonNull List<VisibilityDocument> migratedDocuments)
+ @RequiresNonNull("mAppSearchImpl")
+ private void setLatestSchemaAndDocuments(
+ @UnderInitialization VisibilityStore this,
+ @NonNull List<InternalVisibilityConfig> migratedDocuments)
throws AppSearchException {
// The latest schema type doesn't exist yet. Add it. Set forceOverride true to
// delete old schema.
InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
VISIBILITY_PACKAGE_NAME,
VISIBILITY_DATABASE_NAME,
- Arrays.asList(VisibilityDocument.SCHEMA,
- VisibilityPermissionDocument.SCHEMA),
- /*visibilityDocuments=*/ Collections.emptyList(),
+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
/*forceOverride=*/ true,
- /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
/*setSchemaStatsBuilder=*/ null);
if (!internalSetSchemaResponse.isSuccess()) {
// Impossible case, we just set forceOverride to be true, we should never
@@ -268,15 +327,185 @@
throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
internalSetSchemaResponse.getErrorMessage());
}
+ InternalSetSchemaResponse internalSetAndroidVOverlaySchemaResponse =
+ mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ Collections.singletonList(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityToDocumentConverter
+ .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ if (!internalSetAndroidVOverlaySchemaResponse.isSuccess()) {
+ // Impossible case, we just set forceOverride to be true, we should never
+ // fail in incompatible changes.
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ internalSetAndroidVOverlaySchemaResponse.getErrorMessage());
+ }
for (int i = 0; i < migratedDocuments.size(); i++) {
- VisibilityDocument migratedDocument = migratedDocuments.get(i);
- mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument);
+ InternalVisibilityConfig migratedConfig = migratedDocuments.get(i);
+ mVisibilityConfigMap.put(migratedConfig.getSchemaType(), migratedConfig);
mAppSearchImpl.putDocument(
VISIBILITY_PACKAGE_NAME,
VISIBILITY_DATABASE_NAME,
- migratedDocument.toGenericDocument(),
+ VisibilityToDocumentConverter.createVisibilityDocument(migratedConfig),
/*sendChangeNotifications=*/ false,
/*logger=*/ null);
}
}
+
+ /**
+ * Check and migrate visibility schemas in {@link #ANDROID_V_OVERLAY_DATABASE_NAME} to
+ * {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST}.
+ */
+ @RequiresNonNull("mAppSearchImpl")
+ private void migrateVisibilityOverlayDatabase(@UnderInitialization VisibilityStore this)
+ throws AppSearchException {
+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
+ switch (getSchemaResponse.getVersion()) {
+ case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG:
+ // Force override to next version. This version hasn't released to any public
+ // version. There shouldn't have any public device in this state, so we don't
+ // actually need to migrate any document.
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ Collections.singletonList(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ true, // force update to nest version.
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ if (!internalSetSchemaResponse.isSuccess()) {
+ // Impossible case, we just set forceOverride to be true, we should never
+ // fail in incompatible changes.
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ internalSetSchemaResponse.getErrorMessage());
+ }
+ break;
+ case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO:
+ verifyOrSetLatestVisibilityOverlaySchema(getSchemaResponse);
+ break;
+ default:
+ // We must did something wrong.
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ "Found unsupported visibility version: " + getSchemaResponse.getVersion());
+ }
+ }
+
+ /**
+ * Verify the existing visibility schema, set the latest visibilility schema if it's missing.
+ */
+ @RequiresNonNull("mAppSearchImpl")
+ private void verifyOrSetLatestVisibilitySchema(
+ @UnderInitialization VisibilityStore this, @NonNull GetSchemaResponse getSchemaResponse)
+ throws AppSearchException {
+ // We cannot change the schema version past 2 as detecting version "3" would hit the
+ // default block and throw an AppSearchException. This is why we added
+ // VisibilityOverlay.
+
+ // Check Visibility schema first.
+ Set<AppSearchSchema> existingVisibilitySchema = getSchemaResponse.getSchemas();
+ // Force to override visibility schema if it contains DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA.
+ // The DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA was added to VISIBILITY_DATABASE_NAME and
+ // removed to ANDROID_V_OVERLAY_DATABASE_NAME. We need to force update the schema to
+ // migrate devices that have already store public acl schema.
+ // TODO(b/321326441) remove this method when we no longer to migrate devices in this state.
+ if (existingVisibilitySchema.contains(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)
+ && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA)
+ && existingVisibilitySchema.contains(
+ VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA)) {
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ true,
+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ if (!internalSetSchemaResponse.isSuccess()) {
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ "Fail to force override deprecated visibility schema with public acl.");
+ }
+ } else if (!(existingVisibilitySchema.contains(
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)
+ && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA))) {
+ // We must have a broken schema. Reset it to the latest version.
+ // Do NOT set forceOverride to be true here, see comment below.
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ VISIBILITY_DATABASE_NAME,
+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+ VisibilityPermissionConfig.SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ if (!internalSetSchemaResponse.isSuccess()) {
+ // 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.
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ "Fail to set the latest visibility schema to AppSearch. "
+ + "You may need to update the visibility schema version "
+ + "number.");
+ }
+ }
+ }
+
+ /**
+ * Verify the existing visibility overlay schema, set the latest overlay schema if it's missing.
+ */
+ @RequiresNonNull("mAppSearchImpl")
+ private void verifyOrSetLatestVisibilityOverlaySchema(
+ @UnknownInitialization VisibilityStore this,
+ @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse)
+ throws AppSearchException {
+ // Check Android V overlay schema.
+ Set<AppSearchSchema> existingAndroidVOverlaySchema =
+ getAndroidVOverlaySchemaResponse.getSchemas();
+ if (!existingAndroidVOverlaySchema.contains(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA)) {
+ // We must have a broken schema. Reset it to the latest version.
+ // Do NOT set forceOverride to be true here, see comment below.
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ VISIBILITY_PACKAGE_NAME,
+ ANDROID_V_OVERLAY_DATABASE_NAME,
+ Collections.singletonList(
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+ /*visibilityConfigs=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+ /*setSchemaStatsBuilder=*/ null);
+ if (!internalSetSchemaResponse.isSuccess()) {
+ // If you hit problem here it means you made a incompatible change in
+ // Visibility Schema. You should create new overlay schema
+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ "Fail to set the overlay visibility schema to AppSearch. "
+ + "You may need to create new overlay schema.");
+ }
+ }
+ }
+
+ /**
+ * Whether the given {@link InternalVisibilityConfig} contains Android V overlay settings.
+ *
+ * <p> Android V overlay {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA}
+ * contains public acl and visible to config.
+ */
+ private static boolean isConfigContainsAndroidVOverlay(
+ @Nullable InternalVisibilityConfig config) {
+ return config != null
+ && (config.getVisibilityConfig().getPubliclyVisibleTargetPackage() != null
+ || !config.getVisibleToConfigs().isEmpty());
+ }
}
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
index 4fb4de9..ca255bf 100644
--- 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
@@ -24,7 +24,6 @@
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;
@@ -153,7 +152,7 @@
deprecatedDocuments.add(appSearchImpl.getDocument(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
getDeprecatedVisibilityDocumentId(packageName, databaseName),
/*typePropertyPaths=*/ Collections.emptyMap()));
} catch (AppSearchException e) {
@@ -206,15 +205,15 @@
for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
String prefixedSchemaType = Preconditions.checkNotNull(
deprecatedPackageDocument.getPropertyString(
- DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
+ DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
VisibilityDocumentV1.Builder visibilityBuilder =
getOrCreateBuilder(documentBuilderMap, prefixedSchemaType);
String packageName = Preconditions.checkNotNull(
deprecatedPackageDocument.getPropertyString(
- DEPRECATED_PACKAGE_NAME_PROPERTY));
+ DEPRECATED_PACKAGE_NAME_PROPERTY));
byte[] sha256Cert = Preconditions.checkNotNull(
deprecatedPackageDocument.getPropertyBytes(
- DEPRECATED_SHA_256_CERT_PROPERTY));
+ DEPRECATED_SHA_256_CERT_PROPERTY));
visibilityBuilder.addVisibleToPackage(
new PackageIdentifier(packageName, sha256Cert));
}
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
index 5a082fc..185c19c 100644
--- 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
@@ -20,9 +20,9 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.InternalVisibilityConfig;
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;
@@ -67,7 +67,7 @@
visibilityDocumentV1s.add(new VisibilityDocumentV1(appSearchImpl.getDocument(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
VisibilityStore.VISIBILITY_DATABASE_NAME,
- VisibilityDocument.NAMESPACE,
+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
allPrefixedSchemaTypes.get(i),
/*typePropertyPaths=*/ Collections.emptyMap())));
} catch (AppSearchException e) {
@@ -91,13 +91,13 @@
* @param visibilityDocumentV1s The deprecated Visibility Document we found.
*/
@NonNull
- static List<VisibilityDocument> toVisibilityDocumentsV2(
+ static List<InternalVisibilityConfig> toVisibilityDocumentsV2(
@NonNull List<VisibilityDocumentV1> visibilityDocumentV1s) {
- List<VisibilityDocument> latestVisibilityDocuments =
+ List<InternalVisibilityConfig> 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<Set<Integer>> visibleToPermissionSets = new ArraySet<>();
Set<Integer> deprecatedVisibleToRoles = visibilityDocumentV1.getVisibleToRoles();
if (deprecatedVisibleToRoles != null) {
for (int deprecatedVisibleToRole : deprecatedVisibleToRoles) {
@@ -111,29 +111,28 @@
.READ_ASSISTANT_APP_SEARCH_DATA);
break;
}
- visibleToPermissions.add(visibleToPermission);
+ visibleToPermissionSets.add(visibleToPermission);
}
}
Set<Integer> deprecatedVisibleToPermissions =
visibilityDocumentV1.getVisibleToPermissions();
if (deprecatedVisibleToPermissions != null) {
- visibleToPermissions.add(deprecatedVisibleToPermissions);
+ visibleToPermissionSets.add(deprecatedVisibleToPermissions);
}
- Set<PackageIdentifier> packageIdentifiers = new ArraySet<>();
+ InternalVisibilityConfig.Builder latestVisibilityDocumentBuilder =
+ new InternalVisibilityConfig.Builder(visibilityDocumentV1.getId())
+ .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem());
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]));
+ latestVisibilityDocumentBuilder.addVisibleToPackage(
+ 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);
+ for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+ latestVisibilityDocumentBuilder.addVisibleToPermissions(visibleToPermissions);
}
latestVisibilityDocuments.add(latestVisibilityDocumentBuilder.build());
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
new file mode 100644
index 0000000..c7c5606
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2024 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 android.util.Log;
+
+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.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.collection.ArraySet;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.android.icing.protobuf.InvalidProtocolBufferException;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityToDocumentConverter {
+ private static final String TAG = "AppSearchVisibilityToDo";
+
+ /**
+ * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
+ */
+ public static final String VISIBILITY_DOCUMENT_SCHEMA_TYPE = "VisibilityType";
+ /** Namespace of documents that contain visibility settings */
+ public static final String VISIBILITY_DOCUMENT_NAMESPACE = "";
+
+ /**
+ * The Schema type for the Android V visibility setting overlay documents, that allow for
+ * additional visibility settings.
+ */
+ public static final String ANDROID_V_OVERLAY_SCHEMA_TYPE = "AndroidVOverlayType";
+ /** Namespace of documents that contain Android V visibility setting overlay documents */
+ public static final String ANDROID_V_OVERLAY_NAMESPACE = "androidVOverlay";
+ /** Property that holds the serialized {@link AndroidVOverlayProto}. */
+ public static final String VISIBILITY_PROTO_SERIALIZE_PROPERTY =
+ "visibilityProtoSerializeProperty";
+
+ /**
+ * 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 VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY = "packageName";
+
+ /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+ private static final String VISIBLE_TO_PACKAGE_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 VisibilityConfig contains all visibility information for
+ // whole package.
+ public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+ // One VisibilityConfig contains visibility information for a single schema.
+ public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+ // One VisibilityConfig contains visibility information for a single schema. The permission
+ // visibility information is stored in a document property VisibilityPermissionConfig of the
+ // outer doc.
+ public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+ public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+ // The initial schema version, the overlay schema contains public acl and visible to config
+ // properties.
+ public static final int OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG = 0;
+
+ // The overlay schema only contains a proto property contains all visibility setting.
+ public static final int OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO = 1;
+
+ // The version number of schema saved in Android V overlay database.
+ public static final int ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST =
+ OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO;
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+ */
+ public static final AppSearchSchema VISIBILITY_DOCUMENT_SCHEMA =
+ new AppSearchSchema.Builder(VISIBILITY_DOCUMENT_SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+ NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ PERMISSION_PROPERTY, VisibilityPermissionConfig.SCHEMA_TYPE)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ /** Schema for the VisibilityStore's Android V visibility setting overlay. */
+ public static final AppSearchSchema ANDROID_V_OVERLAY_SCHEMA =
+ new AppSearchSchema.Builder(ANDROID_V_OVERLAY_SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ VISIBILITY_PROTO_SERIALIZE_PROPERTY)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+ /**
+ * The Deprecated schemas and properties that we need to remove from visibility database.
+ * TODO(b/321326441) remove this method when we no longer to migrate devices in this state.
+ */
+ static final AppSearchSchema DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA =
+ new AppSearchSchema.Builder("PublicAclOverlayType")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+ "publiclyVisibleTargetPackage")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+ "publiclyVisibleTargetPackageSha256Cert")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+
+ /**
+ * Constructs a {@link InternalVisibilityConfig} from two {@link GenericDocument}s.
+ *
+ * <p>This constructor is still needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.
+ *
+ * @param visibilityDocument a {@link GenericDocument} holding visibility properties
+ * in {@link #VISIBILITY_DOCUMENT_SCHEMA}
+ * @param androidVOverlayDocument a {@link GenericDocument} holding visibility properties
+ * in {@link #ANDROID_V_OVERLAY_SCHEMA}
+ */
+ @NonNull
+ public static InternalVisibilityConfig createInternalVisibilityConfig(
+ @NonNull GenericDocument visibilityDocument,
+ @Nullable GenericDocument androidVOverlayDocument) {
+ Objects.requireNonNull(visibilityDocument);
+
+ // Parse visibility proto if required
+ AndroidVOverlayProto androidVOverlayProto = null;
+ if (androidVOverlayDocument != null) {
+ try {
+ byte[] androidVOverlayProtoBytes = androidVOverlayDocument.getPropertyBytes(
+ VISIBILITY_PROTO_SERIALIZE_PROPERTY);
+ if (androidVOverlayProtoBytes != null) {
+ androidVOverlayProto = AndroidVOverlayProto.parseFrom(
+ androidVOverlayProtoBytes);
+ }
+ } catch (InvalidProtocolBufferException e) {
+ Log.e(TAG, "Get an invalid android V visibility overlay proto.", e);
+ }
+ }
+
+ // Handle all visibility settings other than visibleToConfigs
+ SchemaVisibilityConfig schemaVisibilityConfig = createVisibilityConfig(
+ visibilityDocument, androidVOverlayProto);
+
+ // Handle visibleToConfigs
+ String schemaType = visibilityDocument.getId();
+ InternalVisibilityConfig.Builder builder = new InternalVisibilityConfig.Builder(schemaType)
+ .setNotDisplayedBySystem(visibilityDocument
+ .getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY))
+ .setVisibilityConfig(schemaVisibilityConfig);
+ if (androidVOverlayProto != null) {
+ List<VisibilityConfigProto> visibleToConfigProtoList =
+ androidVOverlayProto.getVisibleToConfigsList();
+ for (int i = 0; i < visibleToConfigProtoList.size(); i++) {
+ SchemaVisibilityConfig visibleToConfig =
+ convertVisibilityConfigFromProto(visibleToConfigProtoList.get(i));
+ builder.addVisibleToConfig(visibleToConfig);
+ }
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Constructs a {@link SchemaVisibilityConfig} from a {@link GenericDocument} containing legacy
+ * visibility settings, and an {@link AndroidVOverlayProto} containing extended visibility
+ * settings.
+ *
+ * <p>This constructor is still needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.
+ *
+ * @param visibilityDocument a {@link GenericDocument} holding all visibility properties
+ * other than publiclyVisibleTargetPackage.
+ * @param androidVOverlayProto the proto containing post-V visibility settings
+ */
+ @NonNull
+ private static SchemaVisibilityConfig createVisibilityConfig(
+ @NonNull GenericDocument visibilityDocument,
+ @Nullable AndroidVOverlayProto androidVOverlayProto) {
+ Objects.requireNonNull(visibilityDocument);
+
+ // Pre-V visibility settings come from visibilityDocument
+ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();
+
+ String[] visibleToPackageNames =
+ visibilityDocument.getPropertyStringArray(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY);
+ byte[][] visibleToPackageShaCerts =
+ visibilityDocument.getPropertyBytesArray(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY);
+ if (visibleToPackageNames != null && visibleToPackageShaCerts != null) {
+ for (int i = 0; i < visibleToPackageNames.length; i++) {
+ builder.addAllowedPackage(
+ new PackageIdentifier(
+ visibleToPackageNames[i], visibleToPackageShaCerts[i]));
+ }
+ }
+
+ GenericDocument[] visibleToPermissionDocs =
+ visibilityDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);
+ if (visibleToPermissionDocs != null) {
+ for (int i = 0; i < visibleToPermissionDocs.length; ++i) {
+ long[] visibleToPermissionLongs = visibleToPermissionDocs[i].getPropertyLongArray(
+ VisibilityPermissionConfig.ALL_REQUIRED_PERMISSIONS_PROPERTY);
+ if (visibleToPermissionLongs != null) {
+ Set<Integer> allRequiredPermissions =
+ new ArraySet<>(visibleToPermissionLongs.length);
+ for (int j = 0; j < visibleToPermissionLongs.length; j++) {
+ allRequiredPermissions.add((int) visibleToPermissionLongs[j]);
+ }
+ builder.addRequiredPermissions(allRequiredPermissions);
+ }
+ }
+ }
+
+ // Post-V visibility settings come from androidVOverlayProto
+ if (androidVOverlayProto != null) {
+ SchemaVisibilityConfig androidVOverlayConfig =
+ convertVisibilityConfigFromProto(
+ androidVOverlayProto.getVisibilityConfig());
+ builder.setPubliclyVisibleTargetPackage(
+ androidVOverlayConfig.getPubliclyVisibleTargetPackage());
+ }
+
+ return builder.build();
+ }
+
+ @NonNull
+ private static SchemaVisibilityConfig convertVisibilityConfigFromProto(
+ @NonNull VisibilityConfigProto proto) {
+ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();
+
+ List<PackageIdentifierProto> visibleToPackageProtoList = proto.getVisibleToPackagesList();
+ for (int i = 0; i < visibleToPackageProtoList.size(); i++) {
+ PackageIdentifierProto visibleToPackage = proto.getVisibleToPackages(i);
+ builder.addAllowedPackage(convertPackageIdentifierFromProto(visibleToPackage));
+ }
+
+ List<VisibleToPermissionProto> visibleToPermissionProtoList =
+ proto.getVisibleToPermissionsList();
+ for (int i = 0; i < visibleToPermissionProtoList.size(); i++) {
+ VisibleToPermissionProto visibleToPermissionProto = visibleToPermissionProtoList.get(i);
+ Set<Integer> visibleToPermissions =
+ new ArraySet<>(visibleToPermissionProto.getPermissionsList());
+ builder.addRequiredPermissions(visibleToPermissions);
+ }
+
+ if (proto.hasPubliclyVisibleTargetPackage()) {
+ PackageIdentifierProto publiclyVisibleTargetPackage =
+ proto.getPubliclyVisibleTargetPackage();
+ builder.setPubliclyVisibleTargetPackage(
+ convertPackageIdentifierFromProto(publiclyVisibleTargetPackage));
+ }
+
+ return builder.build();
+ }
+
+ private static VisibilityConfigProto convertSchemaVisibilityConfigToProto(
+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+ VisibilityConfigProto.Builder builder = VisibilityConfigProto.newBuilder();
+
+ List<PackageIdentifier> visibleToPackages = schemaVisibilityConfig.getAllowedPackages();
+ for (int i = 0; i < visibleToPackages.size(); i++) {
+ PackageIdentifier visibleToPackage = visibleToPackages.get(i);
+ builder.addVisibleToPackages(convertPackageIdentifierToProto(visibleToPackage));
+ }
+
+ Set<Set<Integer>> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();
+ if (!visibleToPermissions.isEmpty()) {
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ builder.addVisibleToPermissions(
+ VisibleToPermissionProto.newBuilder()
+ .addAllPermissions(allRequiredPermissions));
+ }
+ }
+
+ PackageIdentifier publicAclPackage =
+ schemaVisibilityConfig.getPubliclyVisibleTargetPackage();
+ if (publicAclPackage != null) {
+ builder.setPubliclyVisibleTargetPackage(
+ convertPackageIdentifierToProto(publicAclPackage));
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the {@link GenericDocument} for the visibility schema.
+ *
+ * @param config the configuration to populate into the document
+ */
+ @NonNull
+ public static GenericDocument createVisibilityDocument(
+ @NonNull InternalVisibilityConfig config) {
+ GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
+ VISIBILITY_DOCUMENT_NAMESPACE,
+ config.getSchemaType(), // We are using the prefixedSchemaType to be the id
+ VISIBILITY_DOCUMENT_SCHEMA_TYPE);
+ builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+ config.isNotDisplayedBySystem());
+ SchemaVisibilityConfig schemaVisibilityConfig = config.getVisibilityConfig();
+ List<PackageIdentifier> visibleToPackages = schemaVisibilityConfig.getAllowedPackages();
+ String[] visibleToPackageNames = new String[visibleToPackages.size()];
+ byte[][] visibleToPackageSha256Certs = new byte[visibleToPackages.size()][32];
+ for (int i = 0; i < visibleToPackages.size(); i++) {
+ visibleToPackageNames[i] = visibleToPackages.get(i).getPackageName();
+ visibleToPackageSha256Certs[i] = visibleToPackages.get(i).getSha256Certificate();
+ }
+ builder.setPropertyString(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY, visibleToPackageNames);
+ builder.setPropertyBytes(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY,
+ visibleToPackageSha256Certs);
+
+ // Generate an array of GenericDocument for VisibilityPermissionConfig.
+ Set<Set<Integer>> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();
+ if (!visibleToPermissions.isEmpty()) {
+ GenericDocument[] permissionGenericDocs =
+ new GenericDocument[visibleToPermissions.size()];
+ int i = 0;
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ VisibilityPermissionConfig permissionDocument =
+ new VisibilityPermissionConfig(allRequiredPermissions);
+ permissionGenericDocs[i++] = permissionDocument.toGenericDocument();
+ }
+ builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);
+ }
+
+ // The creationTimestamp doesn't matter for Visibility documents.
+ // But to make tests pass, we set it 0 so two GenericDocuments generated from
+ // the same VisibilityConfig can be same.
+ builder.setCreationTimestampMillis(0L);
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the {@link GenericDocument} for the Android V overlay schema if it is provided,
+ * null otherwise.
+ */
+ @Nullable
+ public static GenericDocument createAndroidVOverlay(
+ @NonNull InternalVisibilityConfig internalVisibilityConfig) {
+ PackageIdentifier publiclyVisibleTargetPackage =
+ internalVisibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();
+ Set<SchemaVisibilityConfig> visibleToConfigs =
+ internalVisibilityConfig.getVisibleToConfigs();
+ if (publiclyVisibleTargetPackage == null && visibleToConfigs.isEmpty()) {
+ // This config doesn't contains any Android V overlay settings
+ return null;
+ }
+
+ VisibilityConfigProto.Builder visibilityConfigProtoBuilder =
+ VisibilityConfigProto.newBuilder();
+ // Set publicAcl
+ if (publiclyVisibleTargetPackage != null) {
+ visibilityConfigProtoBuilder.setPubliclyVisibleTargetPackage(
+ convertPackageIdentifierToProto(publiclyVisibleTargetPackage));
+ }
+
+ // Set visibleToConfigs
+ AndroidVOverlayProto.Builder androidVOverlayProtoBuilder =
+ AndroidVOverlayProto.newBuilder().setVisibilityConfig(visibilityConfigProtoBuilder);
+ if (!visibleToConfigs.isEmpty()) {
+ for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
+ VisibilityConfigProto visibleToConfigProto =
+ convertSchemaVisibilityConfigToProto(visibleToConfig);
+ androidVOverlayProtoBuilder.addVisibleToConfigs(visibleToConfigProto);
+ }
+ }
+
+ GenericDocument.Builder<?> androidVOverlayBuilder = new GenericDocument.Builder<>(
+ ANDROID_V_OVERLAY_NAMESPACE,
+ internalVisibilityConfig.getSchemaType(),
+ ANDROID_V_OVERLAY_SCHEMA_TYPE)
+ .setPropertyBytes(
+ VISIBILITY_PROTO_SERIALIZE_PROPERTY,
+ androidVOverlayProtoBuilder.build().toByteArray());
+
+ // The creationTimestamp doesn't matter for Visibility documents.
+ // But to make tests pass, we set it 0 so two GenericDocuments generated from
+ // the same VisibilityConfig can be same.
+ androidVOverlayBuilder.setCreationTimestampMillis(0L);
+
+ return androidVOverlayBuilder.build();
+ }
+
+ @NonNull
+ private static PackageIdentifierProto convertPackageIdentifierToProto(
+ @NonNull PackageIdentifier packageIdentifier) {
+ return PackageIdentifierProto.newBuilder()
+ .setPackageName(packageIdentifier.getPackageName())
+ .setPackageSha256Cert(ByteString.copyFrom(packageIdentifier.getSha256Certificate()))
+ .build();
+ }
+
+ @NonNull
+ private static PackageIdentifier convertPackageIdentifierFromProto(
+ @NonNull PackageIdentifierProto packageIdentifierProto) {
+ return new PackageIdentifier(
+ packageIdentifierProto.getPackageName(),
+ packageIdentifierProto.getPackageSha256Cert().toByteArray());
+ }
+
+ private VisibilityToDocumentConverter() {}
+}
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
index 69d6a9a..075f44e 100644
--- 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
@@ -54,8 +54,11 @@
Preconditions.checkNotNull(targetPackageName);
Preconditions.checkNotNull(prefixedSchema);
- if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
- return true; // Everyone is always allowed to retrieve their own data.
+ // If the caller is allowed default access to its own data, check if the calling package
+ // and the target package are the same.
+ if (callerAccess.doesCallerHaveSelfAccess()
+ && callerAccess.getCallingPackageName().equals(targetPackageName)) {
+ return true; // Caller is allowed to retrieve its own data.
}
if (visibilityStore == null || visibilityChecker == null) {
return false; // No visibility is configured at this time; no other access possible.
diff --git a/appsearch/appsearch-platform-storage/api/current.txt b/appsearch/appsearch-platform-storage/api/current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/current.txt
+++ b/appsearch/appsearch-platform-storage/api/current.txt
@@ -2,6 +2,7 @@
package androidx.appsearch.platformstorage {
@RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+ method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.EnterpriseGlobalSearchSession!> createEnterpriseGlobalSearchSessionAsync(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 public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
}
diff --git a/appsearch/appsearch-platform-storage/api/restricted_current.txt b/appsearch/appsearch-platform-storage/api/restricted_current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-platform-storage/api/restricted_current.txt
@@ -2,6 +2,7 @@
package androidx.appsearch.platformstorage {
@RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+ method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.EnterpriseGlobalSearchSession!> createEnterpriseGlobalSearchSessionAsync(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 public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
}
diff --git a/appsearch/appsearch-platform-storage/build.gradle b/appsearch/appsearch-platform-storage/build.gradle
index 2009937..11c7bc3 100644
--- a/appsearch/appsearch-platform-storage/build.gradle
+++ b/appsearch/appsearch-platform-storage/build.gradle
@@ -31,10 +31,10 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
- implementation project(":appsearch:appsearch")
+ implementation(project(":appsearch:appsearch"))
+ implementation(project(":core:core"))
implementation('androidx.collection:collection:1.2.0')
implementation("androidx.concurrent:concurrent-futures:1.0.0")
- implementation("androidx.core:core:1.1.0")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
@@ -52,5 +52,6 @@
}
android {
+ compileSdk = 35
namespace "androidx.appsearch.platformstorage"
}
diff --git a/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
new file mode 100644
index 0000000..0e551e4
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2024 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArraySet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SchemaValidationUtilTest {
+ static final int MAX_SECTIONS_ALLOWED = 64;
+
+ @Test
+ public void testValidate_simpleSchemas() {
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new BooleanPropertyConfig.Builder("boolProperty")
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("body")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("age")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).addProperty(new BytesPropertyConfig.Builder("byteProperty")
+ .build()
+ ).build();
+
+ AppSearchSchema[] schemas = {emailSchema, personSchema};
+ // Test that schemas are valid and no exceptions are thrown
+ SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+ MAX_SECTIONS_ALLOWED);
+ }
+
+ @Test
+ public void testValidate_nestedSchemas() {
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("org", "Organization")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("body")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("sender", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("recipient", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).build();
+
+ AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("nickname")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("age")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).build();
+
+ AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+ .addProperty(new StringPropertyConfig.Builder("streetName")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("zipcode")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).build();
+
+ AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).build();
+
+ AppSearchSchema[] schemas = {emailSchema, personSchema, addressSchema, orgSchema};
+ // Test that schemas are valid and no exceptions are thrown
+ SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+ MAX_SECTIONS_ALLOWED);
+ }
+
+ @Test
+ public void testValidate_schemasWithValidCycle() {
+ AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("nickname")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("age")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+ .setShouldIndexNestedProperties(false)
+ .build()
+ ).build();
+
+ AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+ .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+ .addProperty(new StringPropertyConfig.Builder("streetName")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("zipcode")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).build();
+
+ AppSearchSchema[] schemas = {personSchema, orgSchema, addressSchema};
+ // Test that schemas are valid and no exceptions are thrown
+ SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+ MAX_SECTIONS_ALLOWED);
+ }
+
+ @Test
+ public void testValidate_maxSections() {
+ AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+ for (int i = 0; i < MAX_SECTIONS_ALLOWED; i++) {
+ personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ }
+ Set<AppSearchSchema> schemas = new ArraySet<>();
+ schemas.add(personSchemaBuilder.build());
+ // Test that schemas are valid and no exceptions are thrown
+ SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas, MAX_SECTIONS_ALLOWED);
+
+ // Add one more property to bring the number of sections over the max limit
+ personSchemaBuilder.addProperty(new StringPropertyConfig.Builder(
+ "string" + MAX_SECTIONS_ALLOWED + 1)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ schemas.clear();
+ schemas.add(personSchemaBuilder.build());
+ IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+ () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas,
+ MAX_SECTIONS_ALLOWED));
+ assertThat(exception.getMessage()).contains("Too many properties to be indexed");
+ }
+
+ @Test
+ public void testValidate_schemasWithInvalidCycleThrowsError() {
+ AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("nickname")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .build()
+ ).addProperty(new LongPropertyConfig.Builder("age")
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).build();
+
+ AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+ .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).build();
+
+ AppSearchSchema[] schemas = {personSchema, orgSchema};
+ IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+ () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+ MAX_SECTIONS_ALLOWED));
+ assertThat(exception.getMessage()).contains("Invalid cycle");
+ }
+
+ @Test
+ public void testValidate_unknownDocumentConfigThrowsError() {
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("body")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("unknown", "Unknown")
+ .setShouldIndexNestedProperties(true)
+ .build()
+ ).addProperty(new DocumentPropertyConfig.Builder("unknown2", "Unknown")
+ .setShouldIndexNestedProperties(false)
+ .build()
+ ).build();
+
+ AppSearchSchema[] schemas = {emailSchema};
+ IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+ () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+ MAX_SECTIONS_ALLOWED));
+ assertThat(exception.getMessage()).contains("Undefined schema type");
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
new file mode 100644
index 0000000..b3329a1
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 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;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+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.SearchSpecToPlatformConverter;
+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;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link EnterpriseGlobalSearchSession} which proxies to a
+ * platform {@link android.app.appsearch.EnterpriseGlobalSearchSession}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(35)
+// TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+// BuildCompat.isAtLeastV() is removed.
[email protected]
+class EnterpriseGlobalSearchSessionImpl implements EnterpriseGlobalSearchSession {
+ private final android.app.appsearch.EnterpriseGlobalSearchSession mPlatformSession;
+ private final Executor mExecutor;
+ private final Features mFeatures;
+
+ EnterpriseGlobalSearchSessionImpl(
+ @NonNull android.app.appsearch.EnterpriseGlobalSearchSession platformSession,
+ @NonNull Executor executor,
+ @NonNull Features features) {
+ mPlatformSession = Preconditions.checkNotNull(platformSession);
+ mExecutor = Preconditions.checkNotNull(executor);
+ mFeatures = Preconditions.checkNotNull(features);
+ }
+
+ @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);
+ 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(
+ @NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec) {
+ Preconditions.checkNotNull(queryExpression);
+ Preconditions.checkNotNull(searchSpec);
+ android.app.appsearch.SearchResults platformSearchResults =
+ mPlatformSession.search(
+ queryExpression,
+ SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+ return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+ @NonNull String databaseName) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(databaseName);
+ 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;
+ }
+}
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 4d7b3f3..b1c4f4e 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
@@ -16,15 +16,12 @@
package androidx.appsearch.platformstorage;
import android.content.Context;
-import android.content.pm.ModuleInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.os.Build;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.appsearch.app.Features;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
/**
@@ -32,12 +29,6 @@
* level.
*/
final class FeaturesImpl implements Features {
- private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
-
- // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
- // 0 if it is not found, or the version code if it is found.
- private static volatile long sAppSearchVersionCode = -1;
-
// Context is used to check mainline module version, as support varies by module version.
private final Context mContext;
@@ -45,6 +36,9 @@
mContext = Preconditions.checkNotNull(context);
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
public boolean isFeatureSupported(@NonNull String feature) {
switch (feature) {
@@ -80,23 +74,39 @@
case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
- // Beyond Android U features
+ // Android V Features
case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
- // TODO(b/258715421) : Update to reflect support in Android U+ once this feature has
- // an extservices sdk that includes it.
- // fall through
- case Features.SCHEMA_SET_DELETION_PROPAGATION:
- // TODO(b/268521214) : Update when feature is ready in service-appsearch.
// fall through
case Features.SCHEMA_ADD_PARENT_TYPE:
- // TODO(b/269295094) : Update when feature is ready in service-appsearch.
// fall through
case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
- // TODO(b/289150947) : Update when feature is ready in service-appsearch.
// fall through
case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
- // TODO(b/296088047) : Update when feature is ready in service-appsearch.
- return false;
+ // fall through
+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+ // fall through
+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+ // fall through
+ case Features.ENTERPRISE_GLOBAL_SEARCH_SESSION:
+ return BuildCompat.isAtLeastV();
+
+ // Beyond Android V Features
+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+ // TODO(b/326656531) : Update when feature is ready in service-appsearch.
+ // fall through
+ case Features.SCHEMA_SET_DESCRIPTION:
+ // TODO(b/326987971) : Update when feature is ready in service-appsearch.
+ // fall through
+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+ // TODO(b/332620561) : Update when feature is ready in service-appsearch.
+ // fall through
+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
+ // TODO(b/332642571) : Update when feature is ready in service-appsearch.
+ // fall through
default:
return false;
}
@@ -107,53 +117,11 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return 64;
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
- // Sixty-four properties were enabled in mainline module 'aml_ase_331311020'
- return getAppSearchVersionCode(mContext) >= 331311020 ? 64 : 16;
+ // Sixty-four properties were enabled in mainline module of the U base version
+ return AppSearchVersionUtil.getAppSearchVersionCode(mContext)
+ >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE ? 64 : 16;
} else {
return 16;
}
}
-
- @RequiresApi(Build.VERSION_CODES.Q)
- private static long getAppSearchVersionCode(Context context) {
- if (sAppSearchVersionCode != -1) {
- return sAppSearchVersionCode;
- }
- synchronized (FeaturesImpl.class) {
- // Check again in case it was assigned while waiting
- if (sAppSearchVersionCode == -1) {
- long appsearchVersionCode = 0;
- try {
- PackageManager packageManager = context.getPackageManager();
- String appSearchPackageName =
- ApiHelperForQ.getAppSearchPackageName(packageManager);
- if (appSearchPackageName != null) {
- PackageInfo pInfo = packageManager
- .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
- appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
- }
- } catch (PackageManager.NameNotFoundException e) {
- // Module not installed
- }
- sAppSearchVersionCode = appsearchVersionCode;
- }
- }
- return sAppSearchVersionCode;
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- private static class ApiHelperForQ {
- @DoNotInline
- static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
- return pInfo.getLongVersionCode();
- }
-
- @DoNotInline
- static String getAppSearchPackageName(PackageManager packageManager)
- throws PackageManager.NameNotFoundException {
- ModuleInfo appSearchModule =
- packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
- return appSearchModule.getPackageName();
- }
- }
}
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 26c2711c..a5cc45d 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
@@ -47,6 +47,7 @@
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;
@@ -103,6 +104,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public SearchResults search(
@@ -131,6 +135,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
@Override
public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
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 c1ca5d4..0a586c5 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
@@ -20,20 +20,25 @@
import android.content.Context;
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter;
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.concurrent.Executor;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.function.Consumer;
/**
* An AppSearch storage system which stores data in the central AppSearch service, available on
@@ -201,7 +206,8 @@
// execute() won't return anything, we will hang forever waiting for the execution.
// AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
// mutate requests will need to gain write lock and query requests need to gain read lock.
- static final Executor EXECUTOR = Executors.newCachedThreadPool();
+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+ .createCachedThreadPoolExecutor();
/**
* Opens a new {@link AppSearchSession} on this storage.
@@ -224,7 +230,7 @@
if (result.isSuccess()) {
future.set(
new SearchSessionImpl(result.getResultValue(), context.mExecutor,
- new FeaturesImpl(context.mContext)));
+ context.mContext));
} else {
// Without the SuppressLint annotation on the method, this line causes a
// lint error because getResultCode isn't defined as returning a value from
@@ -266,4 +272,59 @@
});
return future;
}
+
+ /**
+ * Opens a new {@link EnterpriseGlobalSearchSession} on this storage.
+ */
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
+ @RequiresApi(35)
+ @SuppressLint("WrongConstant")
+ @NonNull
+ public static ListenableFuture<EnterpriseGlobalSearchSession>
+ createEnterpriseGlobalSearchSessionAsync(@NonNull GlobalSearchContext context) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ Features.ENTERPRISE_GLOBAL_SEARCH_SESSION
+ + " is not supported on this AppSearch implementation");
+ }
+ Preconditions.checkNotNull(context);
+ AppSearchManager appSearchManager =
+ context.mContext.getSystemService(AppSearchManager.class);
+ ResolvableFuture<EnterpriseGlobalSearchSession> future = ResolvableFuture.create();
+ ApiHelperForV.createEnterpriseGlobalSearchSession(
+ appSearchManager,
+ context.mExecutor,
+ result -> {
+ if (result.isSuccess()) {
+ future.set(new EnterpriseGlobalSearchSessionImpl(
+ result.getResultValue(), context.mExecutor,
+ new FeaturesImpl(context.mContext)));
+ } else {
+ // Without the SuppressLint annotation on the method, this line causes a
+ // lint error because getResultCode isn't defined as returning a value from
+ // AppSearchResult.ResultCode
+ future.setException(
+ new AppSearchException(
+ result.getResultCode(), result.getErrorMessage()));
+ }
+ });
+ return future;
+ }
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void createEnterpriseGlobalSearchSession(@NonNull AppSearchManager appSearchManager,
+ @NonNull Executor executor,
+ @NonNull Consumer<android.app.appsearch.AppSearchResult<
+ android.app.appsearch.EnterpriseGlobalSearchSession>> callback) {
+ appSearchManager.createEnterpriseGlobalSearchSession(executor, callback);
+ }
+ }
}
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 d0514b0..d766182 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
@@ -15,8 +15,11 @@
*/
package androidx.appsearch.platformstorage;
+import static androidx.appsearch.platformstorage.util.SchemaValidationUtil.checkSchemasAreValidOrThrow;
+
import android.annotation.SuppressLint;
import android.app.appsearch.AppSearchResult;
+import android.content.Context;
import android.os.Build;
import androidx.annotation.DoNotInline;
@@ -40,6 +43,7 @@
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.app.StorageInfo;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.exceptions.IllegalSchemaException;
import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
@@ -49,8 +53,10 @@
import androidx.appsearch.platformstorage.converter.SearchSuggestionResultToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SearchSuggestionSpecToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
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;
@@ -70,22 +76,39 @@
class SearchSessionImpl implements AppSearchSession {
private final android.app.appsearch.AppSearchSession mPlatformSession;
private final Executor mExecutor;
+ private final Context mContext;
private final Features mFeatures;
SearchSessionImpl(
@NonNull android.app.appsearch.AppSearchSession platformSession,
@NonNull Executor executor,
- @NonNull Features features) {
+ @NonNull Context context) {
mPlatformSession = Preconditions.checkNotNull(platformSession);
mExecutor = Preconditions.checkNotNull(executor);
- mFeatures = Preconditions.checkNotNull(features);
+ mContext = Preconditions.checkNotNull(context);
+ mFeatures = new FeaturesImpl(mContext);
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
+ if (needsSchemaValidation()) {
+ try {
+ checkSchemasAreValidOrThrow(request.getSchemas(),
+ getFeatures().getMaxIndexedProperties());
+ } catch (IllegalSchemaException e) {
+ future.setException(
+ new AppSearchException(
+ androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT,
+ e.getMessage()));
+ return future;
+ }
+ }
mPlatformSession.setSchema(
SetSchemaRequestToPlatformConverter.toPlatformSetSchemaRequest(request),
mExecutor,
@@ -97,6 +120,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
@@ -121,6 +147,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
@@ -149,6 +178,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public SearchResults search(
@@ -163,6 +195,9 @@
return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
@Override
public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
@@ -216,6 +251,9 @@
return future;
}
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@SuppressLint("WrongConstant")
@Override
@NonNull
@@ -318,6 +356,17 @@
mPlatformSession.close();
}
+ private boolean needsSchemaValidation() {
+ long appsearchVersionCode = AppSearchVersionUtil.getAppSearchVersionCode(mContext);
+ // Due to b/300135897, we'd like to validate the schema before sending the setSchema
+ // request to IcingLib on some versions of AppSearch.
+ // For these versions, IcingLib and AppSearch would crash if we try to set an
+ // invalid schema where the number of sections in a schema type exceeds the maximum
+ // limit.
+ return appsearchVersionCode >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE
+ && appsearchVersionCode < AppSearchVersionUtil.APPSEARCH_M2023_11_VERSION_CODE;
+ }
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
static class ApiHelperForU {
private ApiHelperForU() {
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 3775ab7..01882d6 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
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
import androidx.core.util.Preconditions;
@@ -87,6 +89,10 @@
platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]);
}
platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);
+ } else if (property instanceof EmbeddingVector[]) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
} else {
throw new IllegalStateException(
String.format("Property \"%s\" has unsupported value type %s", propertyName,
@@ -143,6 +149,8 @@
}
jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
} else {
+ // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+ // available in platform.
throw new IllegalStateException(
String.format("Property \"%s\" has unsupported value type %s", propertyName,
property.getClass().toString()));
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
index 2fb4a40..577673e 100644
--- 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
@@ -24,9 +24,14 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -43,13 +48,16 @@
* Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
* {@link GetSchemaResponse}.
*/
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static GetSchemaResponse toJetpackGetSchemaResponse(
@NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
Preconditions.checkNotNull(platformResponse);
GetSchemaResponse.Builder jetpackBuilder = new GetSchemaResponse.Builder();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- // Android API level in S-v2 and lower won't have any supported feature.
+ // Android API level in S-v2 and lower won't have visibility information.
jetpackBuilder.setVisibilitySettingSupported(false);
}
for (android.app.appsearch.AppSearchSchema platformSchema : platformResponse.getSchemas()) {
@@ -72,6 +80,29 @@
entry.getValue());
}
}
+
+ if (BuildCompat.isAtLeastV()) {
+ // Convert publicly visible schemas
+ Map<String, PackageIdentifier> publiclyVisibleSchemas =
+ ApiHelperForV.getPubliclyVisibleSchemas(platformResponse);
+ if (!publiclyVisibleSchemas.isEmpty()) {
+ for (Map.Entry<String, PackageIdentifier> entry :
+ publiclyVisibleSchemas.entrySet()) {
+ jetpackBuilder.setPubliclyVisibleSchema(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Convert schemas visible to configs
+ Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+ ApiHelperForV.getSchemasVisibleToConfigs(platformResponse);
+ if (!schemasVisibleToConfigs.isEmpty()) {
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ schemasVisibleToConfigs.entrySet()) {
+ jetpackBuilder.setSchemaTypeVisibleToConfigs(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
return jetpackBuilder.build();
}
@@ -130,4 +161,95 @@
return platformResponse.getRequiredPermissionsForSchemaTypeVisibility();
}
}
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ @NonNull
+ static Map<String, PackageIdentifier> getPubliclyVisibleSchemas(
+ android.app.appsearch.GetSchemaResponse platformResponse) {
+ Map<String, android.app.appsearch.PackageIdentifier> platformPubliclyVisibleSchemas =
+ platformResponse.getPubliclyVisibleSchemas();
+ if (platformPubliclyVisibleSchemas.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, PackageIdentifier> jetpackPubliclyVisibleSchemas =
+ new ArrayMap<>(platformPubliclyVisibleSchemas.size());
+ for (Map.Entry<String, android.app.appsearch.PackageIdentifier> entry :
+ platformPubliclyVisibleSchemas.entrySet()) {
+ jetpackPubliclyVisibleSchemas.put(
+ entry.getKey(),
+ new PackageIdentifier(
+ entry.getValue().getPackageName(),
+ entry.getValue().getSha256Certificate()));
+ }
+ return jetpackPubliclyVisibleSchemas;
+ }
+
+ @DoNotInline
+ @NonNull
+ static Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs(
+ android.app.appsearch.GetSchemaResponse platformResponse) {
+ Map<String, Set<android.app.appsearch.SchemaVisibilityConfig>>
+ platformSchemasVisibleToConfigs =
+ platformResponse.getSchemaTypesVisibleToConfigs();
+ if (platformSchemasVisibleToConfigs.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, Set<SchemaVisibilityConfig>> jetpackSchemasVisibleToConfigs =
+ new ArrayMap<>(platformSchemasVisibleToConfigs.size());
+ for (Map.Entry<String, Set<android.app.appsearch.SchemaVisibilityConfig>> entry :
+ platformSchemasVisibleToConfigs.entrySet()) {
+ Set<SchemaVisibilityConfig> jetpackConfigPerType =
+ new ArraySet<>(entry.getValue().size());
+ for (android.app.appsearch.SchemaVisibilityConfig platformConfigPerType :
+ entry.getValue()) {
+ SchemaVisibilityConfig jetpackConfig =
+ toJetpackSchemaVisibilityConfig(platformConfigPerType);
+ jetpackConfigPerType.add(jetpackConfig);
+ }
+ jetpackSchemasVisibleToConfigs.put(entry.getKey(), jetpackConfigPerType);
+ }
+ return jetpackSchemasVisibleToConfigs;
+ }
+
+ /**
+ * Translates a platform {@link android.app.appsearch.SchemaVisibilityConfig} into a jetpack
+ * {@link SchemaVisibilityConfig}.
+ */
+ @NonNull
+ private static SchemaVisibilityConfig toJetpackSchemaVisibilityConfig(
+ @NonNull android.app.appsearch.SchemaVisibilityConfig platformConfig) {
+ Preconditions.checkNotNull(platformConfig);
+ SchemaVisibilityConfig.Builder jetpackBuilder = new SchemaVisibilityConfig.Builder();
+
+ // Translate allowedPackages
+ List<android.app.appsearch.PackageIdentifier> allowedPackages =
+ platformConfig.getAllowedPackages();
+ for (int i = 0; i < allowedPackages.size(); i++) {
+ jetpackBuilder.addAllowedPackage(new PackageIdentifier(
+ allowedPackages.get(i).getPackageName(),
+ allowedPackages.get(i).getSha256Certificate()));
+ }
+
+ // Translate requiredPermissions
+ for (Set<Integer> requiredPermissions : platformConfig.getRequiredPermissions()) {
+ jetpackBuilder.addRequiredPermissions(requiredPermissions);
+ }
+
+ // Translate publiclyVisibleTargetPackage
+ android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage =
+ platformConfig.getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleTargetPackage != null) {
+ jetpackBuilder.setPubliclyVisibleTargetPackage(
+ new PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+
+ return jetpackBuilder.build();
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
index 83584df..1201088 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
@@ -23,6 +23,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.JoinSpec;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
/**
@@ -38,6 +39,9 @@
*/
@SuppressLint("WrongConstant")
@NonNull
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
public static android.app.appsearch.JoinSpec toPlatformJoinSpec(@NonNull JoinSpec jetpackSpec) {
Preconditions.checkNotNull(jetpackSpec);
return new android.app.appsearch.JoinSpec.Builder(jetpackSpec.getChildPropertyExpression())
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
index f4b2d43..35a4226 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
@@ -18,6 +18,7 @@
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -27,6 +28,7 @@
import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.ReportSystemUsageRequest;
import androidx.appsearch.app.ReportUsageRequest;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.List;
@@ -45,16 +47,37 @@
* Translates a jetpack {@link PutDocumentsRequest} into a platform
* {@link android.app.appsearch.PutDocumentsRequest}.
*/
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest(
@NonNull PutDocumentsRequest jetpackRequest) {
Preconditions.checkNotNull(jetpackRequest);
android.app.appsearch.PutDocumentsRequest.Builder platformBuilder =
new android.app.appsearch.PutDocumentsRequest.Builder();
+ // Convert normal generic documents.
for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
platformBuilder.addGenericDocuments(
GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument));
}
+ // Convert taken action generic documents.
+ for (GenericDocument jetpackTakenActionGenericDocument :
+ jetpackRequest.getTakenActionGenericDocuments()) {
+ if (BuildCompat.isAtLeastV()) {
+ ApiHelperForV.addTakenActionGenericDocuments(
+ platformBuilder,
+ GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+ jetpackTakenActionGenericDocument));
+ } else {
+ // This version of platform-storage doesn't support the dedicated
+ // addTakenActionGenericDocuments API, but we can still add them to the index via
+ // the put API (just without logging).
+ platformBuilder.addGenericDocuments(
+ GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+ jetpackTakenActionGenericDocument));
+ }
+ }
return platformBuilder.build();
}
@@ -71,7 +94,7 @@
jetpackRequest.getNamespace())
.addIds(jetpackRequest.getIds());
for (Map.Entry<String, List<String>> projection :
- jetpackRequest.getProjectionsInternal().entrySet()) {
+ jetpackRequest.getProjections().entrySet()) {
platformBuilder.addProjection(projection.getKey(), projection.getValue());
}
return platformBuilder.build();
@@ -122,4 +145,24 @@
.setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis())
.build();
}
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ static void addTakenActionGenericDocuments(
+ android.app.appsearch.PutDocumentsRequest.Builder platformBuilder,
+ android.app.appsearch.GenericDocument platformTakenActionGenericDocument) {
+ try {
+ platformBuilder.addTakenActionGenericDocuments(platformTakenActionGenericDocument);
+ } catch (android.app.appsearch.exceptions.AppSearchException e) {
+ // This method incorrectly declares that it throws AppSearchException, whereas in
+ // fact there's nothing in its implementation that would do so. Suppress it here
+ // instead of piping all the way through the stack.
+ throw new RuntimeException(
+ "Unexpected AppSearchException which should not be possible", e);
+ }
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
index 5cf9946..b6ebd2d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
@@ -28,8 +28,10 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.Features;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
+import java.util.Collection;
import java.util.List;
/**
@@ -46,23 +48,36 @@
* Translates a jetpack {@link AppSearchSchema} into a platform
* {@link android.app.appsearch.AppSearchSchema}.
*/
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.AppSearchSchema toPlatformSchema(
@NonNull AppSearchSchema jetpackSchema) {
Preconditions.checkNotNull(jetpackSchema);
android.app.appsearch.AppSearchSchema.Builder platformBuilder =
new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());
+ if (!jetpackSchema.getDescription().isEmpty()) {
+ // TODO(b/326987971): Remove this once description becomes available.
+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
+ if (!jetpackSchema.getParentTypes().isEmpty()) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
+ + " is not available on this AppSearch implementation.");
+ }
+ List<String> parentTypes = jetpackSchema.getParentTypes();
+ for (int i = 0; i < parentTypes.size(); i++) {
+ ApiHelperForV.addParentType(platformBuilder, parentTypes.get(i));
+ }
+ }
List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
for (int i = 0; i < properties.size(); i++) {
android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty =
toPlatformProperty(properties.get(i));
platformBuilder.addProperty(platformProperty);
}
- if (!jetpackSchema.getParentTypes().isEmpty()) {
- // TODO(b/269295094): Remove this once polymorphism becomes available.
- throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
- + " is not available on this AppSearch implementation.");
- }
return platformBuilder.build();
}
@@ -70,6 +85,9 @@
* Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
* {@link AppSearchSchema}.
*/
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static AppSearchSchema toJetpackSchema(
@NonNull android.app.appsearch.AppSearchSchema platformSchema) {
@@ -78,22 +96,36 @@
new AppSearchSchema.Builder(platformSchema.getSchemaType());
List<android.app.appsearch.AppSearchSchema.PropertyConfig> properties =
platformSchema.getProperties();
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
+ if (BuildCompat.isAtLeastV()) {
+ List<String> parentTypes = ApiHelperForV.getParentTypes(platformSchema);
+ for (int i = 0; i < parentTypes.size(); i++) {
+ jetpackBuilder.addParentType(parentTypes.get(i));
+ }
+ }
for (int i = 0; i < properties.size(); i++) {
AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
jetpackBuilder.addProperty(jetpackProperty);
}
- // TODO(b/269295094): Call jetpackBuilder.addParentType() to add parent types once
- // polymorphism becomes available in platform.
return jetpackBuilder.build();
}
// Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
// defined as returning the same constants as the corresponding setter expects, but they do
@SuppressLint("WrongConstant")
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
@NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
Preconditions.checkNotNull(jetpackProperty);
+ if (!jetpackProperty.getDescription().isEmpty()) {
+ // TODO(b/326987971): Remove this once description becomes available.
+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
AppSearchSchema.StringPropertyConfig stringProperty =
(AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -110,12 +142,6 @@
TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
}
- if (stringProperty.getDeletionPropagation()) {
- // TODO(b/268521214): Update once deletion propagation is available.
- throw new UnsupportedOperationException("Setting deletion propagation is not "
- + "supported on this AppSearch implementation.");
- }
-
if (stringProperty.getJoinableValueType()
== AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@@ -163,18 +189,26 @@
} else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) {
AppSearchSchema.DocumentPropertyConfig documentProperty =
(AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder platformBuilder =
+ new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
+ documentProperty.getName(), documentProperty.getSchemaType())
+ .setCardinality(documentProperty.getCardinality())
+ .setShouldIndexNestedProperties(
+ documentProperty.shouldIndexNestedProperties());
if (!documentProperty.getIndexableNestedProperties().isEmpty()) {
- // TODO(b/289150947): Update and set list once indexable-nested-properties-list is
- // available.
- throw new UnsupportedOperationException(
- "DocumentPropertyConfig.addIndexableNestedProperties is not supported on "
- + "this AppSearch implementation.");
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ "DocumentPropertyConfig.addIndexableNestedProperties is not supported "
+ + "on this AppSearch implementation.");
+ }
+ ApiHelperForV.addIndexableNestedProperties(
+ platformBuilder, documentProperty.getIndexableNestedProperties());
}
- return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
- documentProperty.getName(), documentProperty.getSchemaType())
- .setCardinality(documentProperty.getCardinality())
- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
- .build();
+ return platformBuilder.build();
+ } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
} else {
throw new IllegalArgumentException(
"Invalid dataType: " + jetpackProperty.getDataType());
@@ -184,6 +218,9 @@
// Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
// defined as returning the same constants as the corresponding setter expects, but they do
@SuppressLint("WrongConstant")
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
private static AppSearchSchema.PropertyConfig toJetpackProperty(
@NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
@@ -201,6 +238,8 @@
jetpackBuilder.setJoinableValueType(
ApiHelperForU.getJoinableValueType(stringProperty));
}
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
return jetpackBuilder.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {
@@ -213,19 +252,27 @@
jetpackBuilder.setIndexingType(
ApiHelperForU.getIndexingType(longProperty));
}
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
return jetpackBuilder.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
.setCardinality(platformProperty.getCardinality())
.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName())
.setCardinality(platformProperty.getCardinality())
.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName())
.setCardinality(platformProperty.getCardinality())
.build();
@@ -233,15 +280,24 @@
instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) {
android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty =
(android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;
- return new AppSearchSchema.DocumentPropertyConfig.Builder(
- documentProperty.getName(),
- documentProperty.getSchemaType())
- .setCardinality(documentProperty.getCardinality())
- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
- .build();
- // TODO(b/289150947): Add the indexable_nested_properties_list once it becomes
- // available in platform.
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in platform.
+ AppSearchSchema.DocumentPropertyConfig.Builder jetpackBuilder =
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ documentProperty.getName(),
+ documentProperty.getSchemaType())
+ .setCardinality(documentProperty.getCardinality())
+ .setShouldIndexNestedProperties(
+ documentProperty.shouldIndexNestedProperties());
+ if (BuildCompat.isAtLeastV()) {
+ List<String> indexableNestedProperties =
+ ApiHelperForV.getIndexableNestedProperties(documentProperty);
+ jetpackBuilder.addIndexableNestedProperties(indexableNestedProperties);
+ }
+ return jetpackBuilder.build();
} else {
+ // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+ // available in platform.
throw new IllegalArgumentException(
"Invalid property type " + platformProperty.getClass()
+ ": " + platformProperty);
@@ -288,4 +344,38 @@
return longPropertyConfig.getIndexingType();
}
}
+
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ @SuppressLint("NewApi")
+ static void addParentType(
+ android.app.appsearch.AppSearchSchema.Builder platformBuilder,
+ @NonNull String parentSchemaType) {
+ platformBuilder.addParentType(parentSchemaType);
+ }
+
+ @DoNotInline
+ static void addIndexableNestedProperties(
+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder
+ platformBuilder,
+ @NonNull Collection<String> indexableNestedProperties) {
+ platformBuilder.addIndexableNestedProperties(indexableNestedProperties);
+ }
+
+ @DoNotInline
+ static List<String> getParentTypes(android.app.appsearch.AppSearchSchema platformSchema) {
+ return platformSchema.getParentTypes();
+ }
+
+ @DoNotInline
+ static List<String> getIndexableNestedProperties(
+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig
+ platformDocumentProperty) {
+ return platformDocumentProperty.getIndexableNestedProperties();
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
index 47bff5a..38c867c 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -26,6 +26,7 @@
import androidx.appsearch.app.Features;
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.SearchSpec;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.List;
@@ -46,6 +47,9 @@
// Most jetpackSearchSpec.get calls cause WrongConstant lint errors because the methods are not
// defined as returning the same constants as the corresponding setter expects, but they do
@SuppressLint("WrongConstant")
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
@NonNull SearchSpec jetpackSearchSpec) {
@@ -77,15 +81,12 @@
.setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
.setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {
- // TODO(b/258715421): Add Build.VERSION.SDK_INT condition once there is an extservices
- // sdk that includes SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA.
- if (true) {
- if ((jetpackSearchSpec.getResultGroupingTypeFlags()
- & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0) {
- throw new UnsupportedOperationException(
+ if ((jetpackSearchSpec.getResultGroupingTypeFlags()
+ & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0
+ && !BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
- + " is not available on this AppSearch implementation.");
- }
+ + " is not available on this AppSearch implementation.");
}
platformBuilder.setResultGrouping(
jetpackSearchSpec.getResultGroupingTypeFlags(),
@@ -107,6 +108,7 @@
}
if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+ // Copy U features
if (jetpackSearchSpec.isNumericSearchEnabled()
|| jetpackSearchSpec.isVerbatimSearchEnabled()
|| jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
@@ -118,6 +120,27 @@
}
ApiHelperForU.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
}
+ // Copy V features
+ if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+ + " is not available on this AppSearch implementation.");
+ }
+ ApiHelperForV.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
+ }
+ // Copy beyond-V features
+ if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+ || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
+ }
+ if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+ // TODO(b/332620561): Remove this once 'tokenize' is supported.
+ throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+ + " is not available on this AppSearch implementation.");
+ }
}
if (jetpackSearchSpec.getJoinSpec() != null) {
@@ -129,9 +152,29 @@
}
if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
- // TODO(b/296088047): Remove this once property filters become available.
- throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
- + " is not available on this AppSearch implementation.");
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+ + " is not available on this AppSearch implementation.");
+ }
+ ApiHelperForV.addFilterProperties(
+ platformBuilder, jetpackSearchSpec.getFilterProperties());
+ }
+
+ if (jetpackSearchSpec.getSearchSourceLogTag() != null) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+ + " is not available on this AppSearch implementation.");
+ }
+ ApiHelperForV.setSearchSourceLogTag(
+ platformBuilder, jetpackSearchSpec.getSearchSourceLogTag());
+ }
+
+ if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+ // TODO(b/332642571): Remove this once informational ranking expressions are available.
+ throw new UnsupportedOperationException(
+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+ + " are not available on this AppSearch implementation.");
}
return platformBuilder.build();
}
@@ -143,6 +186,9 @@
}
@DoNotInline
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
static void setJoinSpec(@NonNull android.app.appsearch.SearchSpec.Builder builder,
JoinSpec jetpackJoinSpec) {
builder.setJoinSpec(JoinSpecToPlatformConverter.toPlatformJoinSpec(jetpackJoinSpec));
@@ -176,4 +222,34 @@
}
}
}
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ static void addFilterProperties(
+ @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+ Map<String, List<String>> properties) {
+ for (Map.Entry<String, List<String>> entry : properties.entrySet()) {
+ platformBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @DoNotInline
+ static void copyEnabledFeatures(
+ @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+ @NonNull SearchSpec jetpackSpec) {
+ if (jetpackSpec.isListFilterHasPropertyFunctionEnabled()) {
+ platformBuilder.setListFilterHasPropertyFunctionEnabled(true);
+ }
+ }
+
+ @DoNotInline
+ static void setSearchSourceLogTag(
+ android.app.appsearch.SearchSpec.Builder platformBuilder,
+ String searchSourceLogTag) {
+ platformBuilder.setSearchSourceLogTag(searchSourceLogTag);
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
index 5479749..ed4a2b2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
@@ -19,12 +19,16 @@
import android.annotation.SuppressLint;
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.Features;
import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -44,6 +48,9 @@
// methods are not defined as returning the same constants as the corresponding setter
// expects, but they do
@SuppressLint("WrongConstant")
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SearchSuggestionSpec toPlatformSearchSuggestionSpec(
@NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
@@ -62,6 +69,32 @@
platformBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
documentIdFilters.getValue());
}
+
+ Map<String, List<String>> jetpackFilterProperties =
+ jetpackSearchSuggestionSpec.getFilterProperties();
+ if (!jetpackFilterProperties.isEmpty()) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+ + " is not available on this AppSearch implementation.");
+ }
+ for (Map.Entry<String, List<String>> entry : jetpackFilterProperties.entrySet()) {
+ ApiHelperForV.addFilterProperties(
+ platformBuilder, entry.getKey(), entry.getValue());
+ }
+ }
return platformBuilder.build();
}
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ static void addFilterProperties(
+ android.app.appsearch.SearchSuggestionSpec.Builder platformBuilder,
+ String schema,
+ Collection<String> propertyPaths) {
+ platformBuilder.addFilterProperties(schema, propertyPaths);
+ }
+ }
}
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 d03d22c..85a1361 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
@@ -26,10 +26,13 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -47,6 +50,9 @@
* Translates a jetpack {@link SetSchemaRequest} into a platform
* {@link android.app.appsearch.SetSchemaRequest}.
*/
+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastV() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
@NonNull SetSchemaRequest jetpackRequest) {
@@ -86,6 +92,38 @@
}
}
}
+
+ if (!jetpackRequest.getPubliclyVisibleSchemas().isEmpty()) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ "Publicly visible schema are not supported on this AppSearch "
+ + "implementation.");
+ }
+ for (Map.Entry<String, PackageIdentifier> entry :
+ jetpackRequest.getPubliclyVisibleSchemas().entrySet()) {
+ PackageIdentifier publiclyVisibleTargetPackage = entry.getValue();
+ ApiHelperForV.setPubliclyVisibleSchema(
+ platformBuilder,
+ entry.getKey(),
+ new android.app.appsearch.PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+ }
+
+ if (!jetpackRequest.getSchemasVisibleToConfigs().isEmpty()) {
+ if (!BuildCompat.isAtLeastV()) {
+ throw new UnsupportedOperationException(
+ "Schema visible to config are not supported on this AppSearch "
+ + "implementation.");
+ }
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ jetpackRequest.getSchemasVisibleToConfigs().entrySet()) {
+ ApiHelperForV.addSchemaTypeVisibleToConfig(
+ platformBuilder, entry.getKey(), entry.getValue());
+ }
+ }
+
for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
Migrator jetpackMigrator = entry.getValue();
android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
@@ -176,4 +214,66 @@
platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(schemaType, permissions);
}
}
+
+ @RequiresApi(35)
+ private static class ApiHelperForV {
+ private ApiHelperForV() {}
+
+ @DoNotInline
+ static void setPubliclyVisibleSchema(
+ android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+ String schemaType,
+ android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage) {
+ platformBuilder.setPubliclyVisibleSchema(schemaType, publiclyVisibleTargetPackage);
+ }
+
+ @DoNotInline
+ public static void addSchemaTypeVisibleToConfig(
+ android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+ String schemaType,
+ Set<SchemaVisibilityConfig> jetpackConfigs) {
+ for (SchemaVisibilityConfig jetpackConfig : jetpackConfigs) {
+ android.app.appsearch.SchemaVisibilityConfig platformConfig =
+ toPlatformSchemaVisibilityConfig(jetpackConfig);
+ platformBuilder.addSchemaTypeVisibleToConfig(schemaType, platformConfig);
+ }
+ }
+
+ /**
+ * Translates a jetpack {@link SchemaVisibilityConfig} into a platform
+ * {@link android.app.appsearch.SchemaVisibilityConfig}.
+ */
+ @NonNull
+ private static android.app.appsearch.SchemaVisibilityConfig
+ toPlatformSchemaVisibilityConfig(@NonNull SchemaVisibilityConfig jetpackConfig) {
+ Preconditions.checkNotNull(jetpackConfig);
+ android.app.appsearch.SchemaVisibilityConfig.Builder platformBuilder =
+ new android.app.appsearch.SchemaVisibilityConfig.Builder();
+
+ // Translate allowedPackages
+ List<PackageIdentifier> allowedPackages = jetpackConfig.getAllowedPackages();
+ for (int i = 0; i < allowedPackages.size(); i++) {
+ platformBuilder.addAllowedPackage(new android.app.appsearch.PackageIdentifier(
+ allowedPackages.get(i).getPackageName(),
+ allowedPackages.get(i).getSha256Certificate()));
+ }
+
+ // Translate requiredPermissions
+ for (Set<Integer> requiredPermissions : jetpackConfig.getRequiredPermissions()) {
+ platformBuilder.addRequiredPermissions(requiredPermissions);
+ }
+
+ // Translate publiclyVisibleTargetPackage
+ PackageIdentifier publiclyVisibleTargetPackage =
+ jetpackConfig.getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleTargetPackage != null) {
+ platformBuilder.setPubliclyVisibleTargetPackage(
+ new android.app.appsearch.PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+
+ return platformBuilder.build();
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
new file mode 100644
index 0000000..3d778ec
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 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.util;
+
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for retrieving platform AppSearch's module version code.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchVersionUtil {
+ public static final long APPSEARCH_U_BASE_VERSION_CODE = 331311020;
+ public static final long APPSEARCH_M2023_11_VERSION_CODE = 341113000;
+
+ private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
+
+ // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
+ // 0 if it is not found, or the version code if it is found.
+ private static volatile long sAppSearchVersionCode = -1;
+
+ private AppSearchVersionUtil() {
+ }
+
+ /**
+ * Returns AppSearch's version code from the context.
+ */
+ @RequiresApi(Build.VERSION_CODES.Q)
+ public static long getAppSearchVersionCode(@NonNull Context context) {
+ Preconditions.checkNotNull(context);
+ if (sAppSearchVersionCode != -1) {
+ return sAppSearchVersionCode;
+ }
+ synchronized (AppSearchVersionUtil.class) {
+ // Check again in case it was assigned while waiting
+ if (sAppSearchVersionCode == -1) {
+ long appsearchVersionCode = 0;
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ String appSearchPackageName =
+ ApiHelperForQ.getAppSearchPackageName(packageManager);
+ if (appSearchPackageName != null) {
+ PackageInfo pInfo = packageManager
+ .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
+ appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Module not installed
+ }
+ sAppSearchVersionCode = appsearchVersionCode;
+ }
+ }
+ return sAppSearchVersionCode;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ private static class ApiHelperForQ {
+ @DoNotInline
+ static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
+ return pInfo.getLongVersionCode();
+ }
+
+ @DoNotInline
+ static String getAppSearchPackageName(PackageManager packageManager)
+ throws PackageManager.NameNotFoundException {
+ ModuleInfo appSearchModule =
+ packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
+ return appSearchModule.getPackageName();
+ }
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
new file mode 100644
index 0000000..7a98515b
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2024 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.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema validation.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaValidationUtil {
+ private SchemaValidationUtil() {
+ }
+
+ /**
+ * Checks that the set of AppSearch schemas form valid schema-type definitions, and do not
+ * exceed the maximum number of indexed properties allowed.
+ *
+ * @param appSearchSchemas Set of AppSearch Schemas for a
+ * {@link androidx.appsearch.app.SetSchemaRequest}.
+ * @param maxSectionsAllowed The maximum number of sections allowed per AppSearch schema.
+ * @throws IllegalSchemaException if any schema in the set is invalid. A schema is invalid if
+ * it contains an invalid cycle, has an undefined document
+ * property config, or exceeds the
+ * maximum number of sections allowed.
+ */
+ public static void checkSchemasAreValidOrThrow(@NonNull Set<AppSearchSchema> appSearchSchemas,
+ int maxSectionsAllowed) throws IllegalSchemaException {
+ Map<String, AppSearchSchema> knownSchemas = new ArrayMap<>();
+ for (AppSearchSchema schema : appSearchSchemas) {
+ knownSchemas.put(schema.getSchemaType(), schema);
+ }
+ Map<String, Integer> cachedNumSectionsMap = new ArrayMap<>();
+ for (AppSearchSchema schema : appSearchSchemas) {
+ // Check that the number of sections is below the max allowed
+ int numSections = getNumSectionsInSchemaOrThrow(schema, knownSchemas,
+ cachedNumSectionsMap, new ArraySet<>());
+ if (numSections > maxSectionsAllowed) {
+ throw new IllegalSchemaException(
+ "Too many properties to be indexed, max " + "number of properties allowed: "
+ + maxSectionsAllowed);
+ }
+ }
+ }
+
+ /**
+ * Returns the number of indexes sections in a given AppSearch schema.
+ *
+ * @param schema The AppSearch schema to get the number of sections for.
+ * @param knownSchemas Map of known schema-type strings to their corresponding
+ * schemas.
+ * @param cachedNumSectionsInSchema Map of the cached number of sections in schemas which have
+ * already been expanded.
+ * @param visitedSchemaTypes Set of schemas that have already been expanded as parents
+ * of the current schema.
+ * @throws IllegalSchemaException if the schema contains an invalid cycle, or contains a
+ * DocumentPropertyConfig where the config's schema type is
+ * unknown.
+ */
+ private static int getNumSectionsInSchemaOrThrow(@NonNull AppSearchSchema schema,
+ @NonNull Map<String, AppSearchSchema> knownSchemas,
+ @NonNull Map<String, Integer> cachedNumSectionsInSchema,
+ @NonNull Set<String> visitedSchemaTypes)
+ throws IllegalSchemaException {
+ String schemaType = schema.getSchemaType();
+ if (visitedSchemaTypes.contains(schemaType)) {
+ // We've hit an illegal cycle where all DocumentPropertyConfigs set
+ // shouldIndexNestedProperties = true.
+ throw new IllegalSchemaException(
+ "Invalid cycle detected in schema type configs. '" + schemaType
+ + "' references itself.");
+ }
+ if (cachedNumSectionsInSchema.containsKey(schemaType)) {
+ // We've already calculated and cached the number of sections in this AppSearch schema,
+ // just return this value.
+ return cachedNumSectionsInSchema.get(schemaType);
+ }
+
+ visitedSchemaTypes.add(schemaType);
+ int numSections = 0;
+ for (PropertyConfig property : schema.getProperties()) {
+ if (property.getDataType() == PropertyConfig.DATA_TYPE_DOCUMENT) {
+ DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) property;
+ String docPropertySchemaType = documentProperty.getSchemaType();
+ if (!knownSchemas.containsKey(docPropertySchemaType)) {
+ // The schema type that this document property config is referring to
+ // does not exist in the provided schemas
+ throw new IllegalSchemaException(
+ "Undefined schema type: " + docPropertySchemaType);
+ }
+ if (!documentProperty.shouldIndexNestedProperties()) {
+ numSections += documentProperty.getIndexableNestedProperties().size();
+ } else {
+ numSections += getNumSectionsInSchemaOrThrow(
+ knownSchemas.get(docPropertySchemaType), knownSchemas,
+ cachedNumSectionsInSchema, visitedSchemaTypes);
+ }
+ } else {
+ numSections += isPropertyIndexable(property) ? 1 : 0;
+ }
+ }
+ visitedSchemaTypes.remove(schemaType);
+ cachedNumSectionsInSchema.put(schemaType, numSections);
+ return numSections;
+ }
+
+ private static boolean isPropertyIndexable(PropertyConfig propertyConfig) {
+ switch (propertyConfig.getDataType()) {
+ case PropertyConfig.DATA_TYPE_STRING:
+ return ((StringPropertyConfig) propertyConfig).getIndexingType()
+ != StringPropertyConfig.INDEXING_TYPE_NONE;
+ case PropertyConfig.DATA_TYPE_LONG:
+ return ((LongPropertyConfig) propertyConfig).getIndexingType()
+ != LongPropertyConfig.INDEXING_TYPE_NONE;
+ case PropertyConfig.DATA_TYPE_DOCUMENT:
+ DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) propertyConfig;
+ return documentProperty.shouldIndexNestedProperties()
+ || !documentProperty.getIndexableNestedProperties().isEmpty();
+ case PropertyConfig.DATA_TYPE_DOUBLE:
+ // fallthrough
+ case PropertyConfig.DATA_TYPE_BOOLEAN:
+ // fallthrough
+ case PropertyConfig.DATA_TYPE_BYTES:
+ // fallthrough
+ default:
+ return false;
+ }
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/build.gradle b/appsearch/appsearch-play-services-storage/build.gradle
index 4de462e..34b64a7 100644
--- a/appsearch/appsearch-play-services-storage/build.gradle
+++ b/appsearch/appsearch-play-services-storage/build.gradle
@@ -30,8 +30,7 @@
dependencies {
implementation project(":appsearch:appsearch")
- // TODO(b/278583111) Swap with the core library version in which Function gets added.
- implementation project(":core:core")
+ implementation("androidx.core:core:1.12.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation('androidx.collection:collection:1.2.0')
implementation("com.google.android.gms:play-services-appsearch:16.0.0", {
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
index 2f580b9..6393a15 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -26,88 +26,68 @@
final class FeaturesImpl implements Features {
@Override
public boolean isFeatureSupported(@NonNull String feature) {
- // TODO(b/274986359): Update based on features available in {@link Features} and those
- // supported by play-services-appsearch.
switch (feature) {
- // Android T Features
case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
// fall through
case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
// fall through
case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
// fall through
+ case Features.JOIN_SPEC_AND_QUALIFIED_ID:
+ // fall through
+ case Features.NUMERIC_SEARCH:
+ // fall through
+ case Features.VERBATIM_SEARCH:
+ // fall through
+ case Features.LIST_FILTER_QUERY_LANGUAGE:
+ // fall through
+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+ // fall through
+ case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+ // fall through
case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
// fall through
- return true; // AppSearch features in T, present in GMSCore AppSearch.
+ case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+ // fall through
+ case Features.TOKENIZER_TYPE_RFC822:
+ // fall through
+ case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+ // fall through
+ case Features.SEARCH_SUGGESTION:
+ // fall through
+ case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
+ // fall through
+ case Features.SCHEMA_ADD_PARENT_TYPE:
+ // fall through
+ case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+ // fall through
+ case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+ // fall through
+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+ // fall through
+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+ return true; // AppSearch features present in GMSCore AppSearch.
// RegisterObserver and UnregisterObserver are not yet supported by GMSCore AppSearch.
// TODO(b/208654892) : Update to reflect support once this feature is supported.
case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
- // Android U Features
- case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
- // TODO(b/203700301) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
// fall through
- case Features.TOKENIZER_TYPE_RFC822:
- // TODO(b/259294369) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
// fall through
- case Features.NUMERIC_SEARCH:
- // TODO(b/259744228) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
+ case Features.SCHEMA_SET_DESCRIPTION:
// fall through
- case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
- // TODO(b/261474063) : Update to reflect support in Android U+ once advanced
- // ranking becomes available.
+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
// fall through
- case Features.JOIN_SPEC_AND_QUALIFIED_ID:
- // TODO(b/256022027) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:
// fall through
- case Features.VERBATIM_SEARCH:
- // TODO(b/204333391) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.LIST_FILTER_QUERY_LANGUAGE:
- // TODO(b/208654892) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
- // TODO(b/258715421) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SEARCH_SUGGESTION:
- // TODO(b/227356108) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SCHEMA_SET_DELETION_PROPAGATION:
- // TODO(b/268521214) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
- // TODO(b/280698121) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SCHEMA_ADD_PARENT_TYPE:
- // TODO(b/269295094) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
- // TODO(b/289150947) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- // fall through
- case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
- // TODO(b/296088047) : Update to reflect support in Android U+ once this feature is
- // synced over into service-appsearch.
- return false;
default:
- return false; // AppSearch features in U+, absent in GMSCore AppSearch.
+ return false; // AppSearch features absent in GMSCore AppSearch.
}
}
@Override
public int getMaxIndexedProperties() {
- // TODO(b/241310816): Update to reflect support in Android U+ once 64 indexable properties
- // are possible in service-appsearch.
- return 16;
+ return 64;
}
}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
index 61c90ac..5bc02ef 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
@@ -56,6 +56,8 @@
private final Features mFeatures;
private final Executor mExecutor;
+ private boolean mIsClosed = false;
+
GlobalSearchSessionImpl(
@NonNull GlobalSearchClient gmsClient,
@NonNull Features features,
@@ -69,6 +71,10 @@
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, "GlobalSearchSession has already been closed");
return AppSearchTaskFutures.toListenableFuture(
mGmsClient.getByDocumentId(packageName, databaseName,
RequestToGmsConverter.toGmsGetByDocumentIdRequest(request)),
@@ -80,6 +86,9 @@
@NonNull
@Override
public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ Preconditions.checkNotNull(queryExpression);
+ Preconditions.checkNotNull(searchSpec);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
com.google.android.gms.appsearch.SearchResults searchResults =
mGmsClient.search(queryExpression,
SearchSpecToGmsConverter.toGmsSearchSpec(searchSpec));
@@ -90,6 +99,8 @@
@Override
public ListenableFuture<Void> reportSystemUsageAsync(
@NonNull ReportSystemUsageRequest request) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
Task<Void> flushTask = Tasks.forResult(null);
return AppSearchTaskFutures.toListenableFuture(flushTask, /* valueMapper= */ i-> i,
mExecutor);
@@ -99,6 +110,9 @@
@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 AppSearchTaskFutures.toListenableFuture(
mGmsClient.getSchema(packageName, databaseName),
GetSchemaResponseToGmsConverter::toJetpackGetSchemaResponse, mExecutor);
@@ -130,6 +144,6 @@
@Override
public void close() {
- mGmsClient.close();
+ mIsClosed = true;
}
}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
index ca2bee2..8a3556d 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
@@ -19,6 +19,7 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
@@ -33,7 +34,6 @@
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
/**
* An AppSearch storage system which stores data in the central AppSearch service in Google
@@ -200,7 +200,8 @@
// execute() won't return anything, we will hang forever waiting for the execution.
// AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
// mutate requests will need to gain write lock and query requests need to gain read lock.
- static final Executor EXECUTOR = Executors.newCachedThreadPool();
+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+ .createCachedThreadPoolExecutor();
/**
* Opens a new {@link AppSearchSession} on this storage.
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
index df99af7..998ded9 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
@@ -40,6 +40,8 @@
import androidx.appsearch.playservicesstorage.converter.RequestToGmsConverter;
import androidx.appsearch.playservicesstorage.converter.ResponseToGmsConverter;
import androidx.appsearch.playservicesstorage.converter.SearchSpecToGmsConverter;
+import androidx.appsearch.playservicesstorage.converter.SearchSuggestionResultToGmsConverter;
+import androidx.appsearch.playservicesstorage.converter.SearchSuggestionSpecToGmsConverter;
import androidx.appsearch.playservicesstorage.converter.SetSchemaRequestToGmsConverter;
import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
import androidx.core.util.Preconditions;
@@ -149,9 +151,16 @@
public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
@NonNull String suggestionQueryExpression,
@NonNull SearchSuggestionSpec searchSuggestionSpec) {
- // TODO(b/274986359): Implement searchSuggestionAsync for PlayServicesStorage.
- throw new UnsupportedOperationException(
- "Search Suggestion is not yet supported on this AppSearch implementation.");
+ Preconditions.checkNotNull(suggestionQueryExpression);
+ Preconditions.checkNotNull(searchSuggestionSpec);
+ return AppSearchTaskFutures.toListenableFuture(
+ mGmsClient.searchSuggestion(
+ suggestionQueryExpression,
+ SearchSuggestionSpecToGmsConverter.toGmsSearchSuggestionSpec(
+ searchSuggestionSpec),
+ mDatabaseName),
+ SearchSuggestionResultToGmsConverter :: toGmsSearchSuggestionResults,
+ mExecutor);
}
@NonNull
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
index 2b78fbf..0aec84e 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
@@ -18,6 +18,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
import androidx.core.util.Preconditions;
@@ -71,6 +73,10 @@
}
gmsBuilder.setPropertyDocument(propertyName,
gmsSubDocuments);
+ } else if (property instanceof EmbeddingVector[]) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
} else {
throw new IllegalStateException(
String.format("Property \"%s\" has unsupported value type %s",
@@ -121,6 +127,8 @@
}
jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
} else {
+ // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+ // available in gms-appsearch.
throw new IllegalStateException(
String.format("Property \"%s\" has unsupported value type %s", propertyName,
property.getClass().toString()));
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
index bacb0b8..bb37609 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
@@ -20,9 +20,13 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -67,9 +71,73 @@
jetpackBuilder.setRequiredPermissionsForSchemaTypeVisibility(entry.getKey(),
entry.getValue());
}
+ // Convert publicly visible schemas
+ Map<String, PackageIdentifier> publiclyVisibleSchemas =
+ getPubliclyVisibleSchemas(gmsResponse);
+ if (!publiclyVisibleSchemas.isEmpty()) {
+ for (Map.Entry<String, PackageIdentifier> entry :
+ publiclyVisibleSchemas.entrySet()) {
+ jetpackBuilder.setPubliclyVisibleSchema(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Convert schemas visible to configs
+ Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+ getSchemasVisibleToConfigs(gmsResponse);
+ if (!schemasVisibleToConfigs.isEmpty()) {
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ schemasVisibleToConfigs.entrySet()) {
+ jetpackBuilder.setSchemaTypeVisibleToConfigs(entry.getKey(), entry.getValue());
+ }
+ }
return jetpackBuilder.build();
}
+ private static Map<String, PackageIdentifier> getPubliclyVisibleSchemas(
+ com.google.android.gms.appsearch.GetSchemaResponse gmsResponse) {
+ Map<String, com.google.android.gms.appsearch.PackageIdentifier>
+ gmsPubliclyVisibleSchemas = gmsResponse.getPubliclyVisibleSchemas();
+ if (gmsPubliclyVisibleSchemas.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, PackageIdentifier> jetpackPubliclyVisibleSchemas =
+ new ArrayMap<>(gmsPubliclyVisibleSchemas.size());
+ for (Map.Entry<String, com.google.android.gms.appsearch.PackageIdentifier> entry :
+ gmsPubliclyVisibleSchemas.entrySet()) {
+ jetpackPubliclyVisibleSchemas.put(
+ entry.getKey(),
+ new PackageIdentifier(
+ entry.getValue().getPackageName(),
+ entry.getValue().getSha256Certificate()));
+ }
+ return jetpackPubliclyVisibleSchemas;
+ }
+
+ private static Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs(
+ com.google.android.gms.appsearch.GetSchemaResponse gmsResponse) {
+ Map<String, Set<com.google.android.gms.appsearch.SchemaVisibilityConfig>>
+ gmsSchemasVisibleToConfigs =
+ gmsResponse.getSchemaTypesVisibleToConfigs();
+ if (gmsSchemasVisibleToConfigs.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, Set<SchemaVisibilityConfig>> jetpackSchemasVisibleToConfigs =
+ new ArrayMap<>(gmsSchemasVisibleToConfigs.size());
+ for (Map.Entry<String, Set<com.google.android.gms.appsearch.SchemaVisibilityConfig>> entry :
+ gmsSchemasVisibleToConfigs.entrySet()) {
+ Set<SchemaVisibilityConfig> jetpackConfigPerType =
+ new ArraySet<>(entry.getValue().size());
+ for (com.google.android.gms.appsearch.SchemaVisibilityConfig gmsConfigPerType :
+ entry.getValue()) {
+ SchemaVisibilityConfig jetpackConfig =
+ toJetpackSchemaVisibilityConfig(gmsConfigPerType);
+ jetpackConfigPerType.add(jetpackConfig);
+ }
+ jetpackSchemasVisibleToConfigs.put(entry.getKey(), jetpackConfigPerType);
+ }
+ return jetpackSchemasVisibleToConfigs;
+ }
+
/**
* Adds package visibilities in a Gms
* {@link com.google.android.gms.appsearch.GetSchemaResponse} into
@@ -97,4 +165,42 @@
}
}
}
+
+ /**
+ * Translates a platform {@link com.google.android.gms.appsearch.SchemaVisibilityConfig} into
+ * a jetpack
+ * {@link SchemaVisibilityConfig}.
+ */
+ @NonNull
+ private static SchemaVisibilityConfig toJetpackSchemaVisibilityConfig(
+ @NonNull com.google.android.gms.appsearch.SchemaVisibilityConfig platformConfig) {
+ Preconditions.checkNotNull(platformConfig);
+ SchemaVisibilityConfig.Builder jetpackBuilder = new SchemaVisibilityConfig.Builder();
+
+ // Translate allowedPackages
+ List<com.google.android.gms.appsearch.PackageIdentifier> allowedPackages =
+ platformConfig.getAllowedPackages();
+ for (int i = 0; i < allowedPackages.size(); i++) {
+ jetpackBuilder.addAllowedPackage(new PackageIdentifier(
+ allowedPackages.get(i).getPackageName(),
+ allowedPackages.get(i).getSha256Certificate()));
+ }
+
+ // Translate requiredPermissions
+ for (Set<Integer> requiredPermissions : platformConfig.getRequiredPermissions()) {
+ jetpackBuilder.addRequiredPermissions(requiredPermissions);
+ }
+
+ // Translate publiclyVisibleTargetPackage
+ com.google.android.gms.appsearch.PackageIdentifier publiclyVisibleTargetPackage =
+ platformConfig.getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleTargetPackage != null) {
+ jetpackBuilder.setPubliclyVisibleTargetPackage(
+ new PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+
+ return jetpackBuilder.build();
+ }
}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java
new file mode 100644
index 0000000..08d661f
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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.playservicesstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.JoinSpec;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link JoinSpec}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class JoinSpecToGmsConverter {
+ private JoinSpecToGmsConverter() {
+ }
+
+ /**
+ * Translates a Jetpack {@link JoinSpec} into a gms
+ * {@link com.google.android.gms.appsearch.JoinSpec}.
+ */
+ @SuppressLint("WrongConstant")
+ @NonNull
+ public static com.google.android.gms.appsearch.JoinSpec toGmsJoinSpec(
+ @NonNull JoinSpec jetpackSpec) {
+ Preconditions.checkNotNull(jetpackSpec);
+ return new com.google.android.gms.appsearch.JoinSpec
+ .Builder(jetpackSpec.getChildPropertyExpression())
+ .setNestedSearch(
+ jetpackSpec.getNestedQuery(),
+ SearchSpecToGmsConverter.toGmsSearchSpec(
+ jetpackSpec.getNestedSearchSpec()))
+ .setMaxJoinedResultCount(jetpackSpec.getMaxJoinedResultCount())
+ .setAggregationScoringStrategy(jetpackSpec.getAggregationScoringStrategy())
+ .build();
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
index 34cebb8..8c6f88a 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
@@ -47,10 +47,18 @@
Preconditions.checkNotNull(jetpackRequest);
com.google.android.gms.appsearch.PutDocumentsRequest.Builder gmsBuilder =
new com.google.android.gms.appsearch.PutDocumentsRequest.Builder();
+ // Convert normal generic documents.
for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
gmsBuilder.addGenericDocuments(
+ GenericDocumentToGmsConverter.toGmsGenericDocument(jetpackDocument));
+ }
+ // Convert taken action generic documents.
+ for (GenericDocument jetpackTakenActionGenericDocument :
+ jetpackRequest.getTakenActionGenericDocuments()) {
+ gmsBuilder.addTakenActionGenericDocuments(
GenericDocumentToGmsConverter.toGmsGenericDocument(
- jetpackDocument));
+ jetpackTakenActionGenericDocument)
+ );
}
return gmsBuilder.build();
}
@@ -68,7 +76,7 @@
jetpackRequest.getNamespace())
.addIds(jetpackRequest.getIds());
for (Map.Entry<String, List<String>> projection :
- jetpackRequest.getProjectionsInternal().entrySet()) {
+ jetpackRequest.getProjections().entrySet()) {
gmsBuilder.addProjection(projection.getKey(), projection.getValue());
}
return gmsBuilder.build();
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
index 3c507a5..a777688 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.Features;
import androidx.core.util.Preconditions;
import java.util.List;
@@ -44,6 +45,17 @@
com.google.android.gms.appsearch.AppSearchSchema.Builder gmsBuilder =
new com.google.android.gms.appsearch.AppSearchSchema
.Builder(jetpackSchema.getSchemaType());
+ if (!jetpackSchema.getDescription().isEmpty()) {
+ // TODO(b/326987971): Remove this once description becomes available.
+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
+ if (!jetpackSchema.getParentTypes().isEmpty()) {
+ List<String> parentTypes = jetpackSchema.getParentTypes();
+ for (int i = 0; i < parentTypes.size(); i++) {
+ gmsBuilder.addParentType(parentTypes.get(i));
+ }
+ }
List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
for (int i = 0; i < properties.size(); i++) {
com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig gmsProperty =
@@ -64,8 +76,14 @@
Preconditions.checkNotNull(gmsSchema);
AppSearchSchema.Builder jetpackBuilder =
new AppSearchSchema.Builder(gmsSchema.getSchemaType());
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
List<com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig> properties =
gmsSchema.getProperties();
+ List<String> parentTypes = gmsSchema.getParentTypes();
+ for (int i = 0; i < parentTypes.size(); i++) {
+ jetpackBuilder.addParentType(parentTypes.get(i));
+ }
for (int i = 0; i < properties.size(); i++) {
AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
jetpackBuilder.addProperty(jetpackProperty);
@@ -77,6 +95,11 @@
private static com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig toGmsProperty(
@NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
Preconditions.checkNotNull(jetpackProperty);
+ if (!jetpackProperty.getDescription().isEmpty()) {
+ // TODO(b/326987971): Remove this once description becomes available.
+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
AppSearchSchema.StringPropertyConfig stringProperty =
(AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -87,18 +110,9 @@
.setCardinality(stringProperty.getCardinality())
.setIndexingType(stringProperty.getIndexingType())
.setTokenizerType(stringProperty.getTokenizerType());
- if (stringProperty.getDeletionPropagation()) {
- // TODO(b/268521214): Update once deletion propagation is available.
- throw new UnsupportedOperationException("Setting deletion propagation is not "
- + "supported on this AppSearch implementation.");
- }
-
if (stringProperty.getJoinableValueType()
== AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
- //TODO(b/274986359) Add GMSCore feature check for Joins once available.
- throw new UnsupportedOperationException(
- "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported"
- + " on this AppSearch implementation.");
+ gmsBuilder.setJoinableValueType(stringProperty.getJoinableValueType());
}
return gmsBuilder.build();
} else if (jetpackProperty instanceof AppSearchSchema.LongPropertyConfig) {
@@ -111,10 +125,7 @@
.setCardinality(jetpackProperty.getCardinality());
if (longProperty.getIndexingType()
== AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE) {
- //TODO(b/274986359) Add GMSCore feature check for Indexing Range once available.
- throw new UnsupportedOperationException(
- "LongProperty.INDEXING_TYPE_RANGE is not supported on this AppSearch "
- + "implementation.");
+ longPropertyBuilder.setIndexingType(longProperty.getIndexingType());
}
return longPropertyBuilder.build();
} else if (jetpackProperty instanceof AppSearchSchema.DoublePropertyConfig) {
@@ -139,11 +150,16 @@
AppSearchSchema.DocumentPropertyConfig documentProperty =
(AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
return new com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig
- .Builder(
- documentProperty.getName(), documentProperty.getSchemaType())
+ .Builder(documentProperty.getName(), documentProperty.getSchemaType())
.setCardinality(documentProperty.getCardinality())
- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
- .build();
+ .setShouldIndexNestedProperties(
+ documentProperty.shouldIndexNestedProperties())
+ .addIndexableNestedProperties(
+ documentProperty.getIndexableNestedProperties()).build();
+ } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
} else {
throw new IllegalArgumentException(
"Invalid dataType: " + jetpackProperty.getDataType());
@@ -160,31 +176,45 @@
com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
(com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig)
gmsProperty;
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
.setCardinality(stringProperty.getCardinality())
.setIndexingType(stringProperty.getIndexingType())
.setTokenizerType(stringProperty.getTokenizerType())
+ .setJoinableValueType(stringProperty.getJoinableValueType())
.build();
} else if (gmsProperty
instanceof com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig) {
+ com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig longProperty =
+ (com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig)
+ gmsProperty;
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.LongPropertyConfig.Builder(
gmsProperty.getName())
.setCardinality(gmsProperty.getCardinality())
+ .setIndexingType(longProperty.getIndexingType())
.build();
} else if (gmsProperty
instanceof com.google.android.gms.appsearch.AppSearchSchema.DoublePropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.DoublePropertyConfig.Builder(
gmsProperty.getName())
- .setCardinality(gmsProperty.getCardinality())
- .build();
+ .setCardinality(gmsProperty.getCardinality()).build();
} else if (gmsProperty
instanceof com.google.android.gms.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.BooleanPropertyConfig.Builder(
gmsProperty.getName())
.setCardinality(gmsProperty.getCardinality())
.build();
} else if (gmsProperty
instanceof com.google.android.gms.appsearch.AppSearchSchema.BytesPropertyConfig) {
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.BytesPropertyConfig.Builder(
gmsProperty.getName())
.setCardinality(gmsProperty.getCardinality())
@@ -196,13 +226,20 @@
documentProperty =
(com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig)
gmsProperty;
+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+ // available in gms.
return new AppSearchSchema.DocumentPropertyConfig.Builder(
documentProperty.getName(),
documentProperty.getSchemaType())
.setCardinality(documentProperty.getCardinality())
- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
+ .setShouldIndexNestedProperties(
+ documentProperty.shouldIndexNestedProperties())
+ .addIndexableNestedProperties(
+ documentProperty.getIndexableNestedProperties())
.build();
} else {
+ // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+ // available in gms-appsearch.
throw new IllegalArgumentException(
"Invalid property type " + gmsProperty.getClass()
+ ": " + gmsProperty);
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
index 81a0206..91afc41 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
@@ -68,6 +68,10 @@
gmsMatches.get(i));
builder.addMatchInfo(jetpackMatchInfo);
}
+ for (com.google.android.gms.appsearch.SearchResult joinedResult :
+ gmsResult.getJoinedResults()) {
+ builder.addJoinedResult(toJetpackSearchResult(joinedResult));
+ }
return builder.build();
}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
index 1b7701b..0d687c0 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
@@ -27,7 +27,6 @@
/**
* Translates between Gms and Jetpack versions of {@link SearchSpec}.
-
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SearchSpecToGmsConverter {
@@ -43,10 +42,7 @@
new com.google.android.gms.appsearch.SearchSpec.Builder();
if (!jetpackSearchSpec.getAdvancedRankingExpression().isEmpty()) {
- //TODO(b/274986359) Add GMSCore feature check for Advanced Ranking once available.
- throw new UnsupportedOperationException(
- Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION
- + " is not available on this AppSearch implementation.");
+ gmsBuilder.setRankingStrategy(jetpackSearchSpec.getAdvancedRankingExpression());
} else {
gmsBuilder.setRankingStrategy(jetpackSearchSpec.getRankingStrategy());
}
@@ -57,7 +53,6 @@
.addFilterNamespaces(jetpackSearchSpec.getFilterNamespaces())
.addFilterPackageNames(jetpackSearchSpec.getFilterPackageNames())
.setResultCountPerPage(jetpackSearchSpec.getResultCountPerPage())
- .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())
.setOrder(jetpackSearchSpec.getOrder())
.setSnippetCount(jetpackSearchSpec.getSnippetCount())
.setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
@@ -72,36 +67,61 @@
gmsBuilder.addProjection(projection.getKey(), projection.getValue());
}
- if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
- if (jetpackSearchSpec.isNumericSearchEnabled()
- || jetpackSearchSpec.isVerbatimSearchEnabled()
- || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
- //TODO(b/274986359) Add GMSCore feature check for NUMERIC_SEARCH,
- // VERBATIM_SEARCH and LIST_FILTER_QUERY_LANGUAGE once available.
- throw new UnsupportedOperationException(
- "Advanced query features (NUMERIC_SEARCH, VERBATIM_SEARCH and "
- + "LIST_FILTER_QUERY_LANGUAGE) are not supported with this "
- + "backend/Android API level combination.");
+ if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
+ for (Map.Entry<String, Map<String, Double>> entry :
+ jetpackSearchSpec.getPropertyWeights().entrySet()) {
+ gmsBuilder.setPropertyWeights(entry.getKey(), entry.getValue());
}
}
- if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
- //TODO(b/274986359) Add GMSCore feature check for Property Weights once available.
- throw new UnsupportedOperationException(
- "Property weights are not supported with this backend/Android API level "
- + "combination.");
+ if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+ if (jetpackSearchSpec.isNumericSearchEnabled()) {
+ gmsBuilder.setNumericSearchEnabled(true);
+ }
+ if (jetpackSearchSpec.isVerbatimSearchEnabled()) {
+ gmsBuilder.setVerbatimSearchEnabled(true);
+ }
+ if (jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
+ gmsBuilder.setListFilterQueryLanguageEnabled(true);
+ }
+ if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+ gmsBuilder.setListFilterHasPropertyFunctionEnabled(true);
+ }
+ // Copy beyond-V features
+ if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+ || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+ // TODO(b/326656531): Remove this once embedding search APIs are available.
+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
+ }
+ if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+ // TODO(b/332620561): Remove this once 'tokenize' is supported.
+ throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+ + " is not available on this AppSearch implementation.");
+ }
}
if (jetpackSearchSpec.getJoinSpec() != null) {
- //TODO(b/274986359) Add GMSCore feature check for Joins once available.
- throw new UnsupportedOperationException("JoinSpec is not available on this "
- + "AppSearch implementation.");
+ gmsBuilder.setJoinSpec(JoinSpecToGmsConverter.toGmsJoinSpec(
+ jetpackSearchSpec.getJoinSpec()));
}
if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
- // TODO(b/296088047): Remove this once property filters become available.
- throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
- + " is not available on this AppSearch implementation.");
+ for (Map.Entry<String, List<String>> entry :
+ jetpackSearchSpec.getFilterProperties().entrySet()) {
+ gmsBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+ }
+ }
+
+ if (jetpackSearchSpec.getSearchSourceLogTag() != null) {
+ gmsBuilder.setSearchSourceLogTag(jetpackSearchSpec.getSearchSourceLogTag());
+ }
+
+ if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+ // TODO(b/332642571): Remove this once informational ranking expressions are available.
+ throw new UnsupportedOperationException(
+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+ + " are not available on this AppSearch implementation.");
}
return gmsBuilder.build();
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java
new file mode 100644
index 0000000..f5b8568
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 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.playservicesstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link SearchSuggestionResult}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchSuggestionResultToGmsConverter {
+ private SearchSuggestionResultToGmsConverter() {}
+
+ /** Translates from Platform to Jetpack versions of {@linkSearchSuggestionResult} */
+ @NonNull
+ public static List<SearchSuggestionResult> toGmsSearchSuggestionResults(
+ @NonNull List<com.google.android.gms.appsearch.SearchSuggestionResult>
+ gmsSearchSuggestionResults) {
+ Preconditions.checkNotNull(gmsSearchSuggestionResults);
+ List<SearchSuggestionResult> jetpackSearchSuggestionResults =
+ new ArrayList<>(gmsSearchSuggestionResults.size());
+ for (int i = 0; i < gmsSearchSuggestionResults.size(); i++) {
+ jetpackSearchSuggestionResults.add(new SearchSuggestionResult.Builder()
+ .setSuggestedResult(gmsSearchSuggestionResults.get(i).getSuggestedResult())
+ .build());
+ }
+ return jetpackSearchSuggestionResults;
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java
new file mode 100644
index 0000000..18556a7
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 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.playservicesstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link SearchSuggestionSpec}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchSuggestionSpecToGmsConverter {
+ private SearchSuggestionSpecToGmsConverter() {
+ }
+
+ /** Translates from Jetpack to Gms version of {@link SearchSuggestionSpec}. */
+ // Most jetpackSearchSuggestionSpec.get calls cause WrongConstant lint errors because the
+ // methods are not defined as returning the same constants as the corresponding setter
+ // expects, but they do
+ @SuppressLint("WrongConstant")
+ @NonNull
+ public static com.google.android.gms.appsearch.SearchSuggestionSpec toGmsSearchSuggestionSpec(
+ @NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
+ Preconditions.checkNotNull(jetpackSearchSuggestionSpec);
+
+ com.google.android.gms.appsearch.SearchSuggestionSpec.Builder gmsBuilder =
+ new com.google.android.gms.appsearch.SearchSuggestionSpec.Builder(
+ jetpackSearchSuggestionSpec.getMaximumResultCount());
+
+ gmsBuilder
+ .addFilterNamespaces(jetpackSearchSuggestionSpec.getFilterNamespaces())
+ .addFilterSchemas(jetpackSearchSuggestionSpec.getFilterSchemas())
+ .setRankingStrategy(jetpackSearchSuggestionSpec.getRankingStrategy());
+ for (Map.Entry<String, List<String>> documentIdFilters :
+ jetpackSearchSuggestionSpec.getFilterDocumentIds().entrySet()) {
+ gmsBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
+ documentIdFilters.getValue());
+ }
+
+ Map<String, List<String>> jetpackFilterProperties =
+ jetpackSearchSuggestionSpec.getFilterProperties();
+ if (!jetpackFilterProperties.isEmpty()) {
+ for (Map.Entry<String, List<String>> entry : jetpackFilterProperties.entrySet()) {
+ gmsBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+ }
+ }
+ return gmsBuilder.build();
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
index 3929b54..348849c 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
@@ -22,16 +22,17 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.SetSchemaResponse;
import androidx.core.util.Preconditions;
+import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Translates between Gms and Jetpack versions of {@link SetSchemaRequest}.
-
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SetSchemaRequestToGmsConverter {
@@ -76,6 +77,28 @@
}
}
}
+ if (!jetpackRequest.getPubliclyVisibleSchemas().isEmpty()) {
+ for (Map.Entry<String, PackageIdentifier> entry :
+ jetpackRequest.getPubliclyVisibleSchemas().entrySet()) {
+ PackageIdentifier publiclyVisibleTargetPackage = entry.getValue();
+ gmsBuilder.setPubliclyVisibleSchema(
+ entry.getKey(),
+ new com.google.android.gms.appsearch.PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+ }
+
+ if (!jetpackRequest.getSchemasVisibleToConfigs().isEmpty()) {
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ jetpackRequest.getSchemasVisibleToConfigs().entrySet()) {
+ for (SchemaVisibilityConfig jetpackConfig : entry.getValue()) {
+ com.google.android.gms.appsearch.SchemaVisibilityConfig gmsConfig =
+ toGmsSchemaVisibilityConfig(jetpackConfig);
+ gmsBuilder.addSchemaTypeVisibleToConfig(entry.getKey(), gmsConfig);
+ }
+ }
+ }
for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
Migrator jetpackMigrator = entry.getValue();
com.google.android.gms.appsearch.Migrator gmsMigrator =
@@ -161,4 +184,41 @@
}
return jetpackBuilder.build();
}
+
+ /**
+ * Translates a jetpack {@link SchemaVisibilityConfig} into a gms
+ * {@link com.google.android.gms.appsearch.SchemaVisibilityConfig}.
+ */
+ @NonNull
+ private static com.google.android.gms.appsearch.SchemaVisibilityConfig
+ toGmsSchemaVisibilityConfig(@NonNull SchemaVisibilityConfig jetpackConfig) {
+ Preconditions.checkNotNull(jetpackConfig);
+ com.google.android.gms.appsearch.SchemaVisibilityConfig.Builder gmsBuilder =
+ new com.google.android.gms.appsearch.SchemaVisibilityConfig.Builder();
+
+ // Translate allowedPackages
+ List<PackageIdentifier> allowedPackages = jetpackConfig.getAllowedPackages();
+ for (int i = 0; i < allowedPackages.size(); i++) {
+ gmsBuilder.addAllowedPackage(new com.google.android.gms.appsearch.PackageIdentifier(
+ allowedPackages.get(i).getPackageName(),
+ allowedPackages.get(i).getSha256Certificate()));
+ }
+
+ // Translate requiredPermissions
+ for (Set<Integer> requiredPermissions : jetpackConfig.getRequiredPermissions()) {
+ gmsBuilder.addRequiredPermissions(requiredPermissions);
+ }
+
+ // Translate publiclyVisibleTargetPackage
+ PackageIdentifier publiclyVisibleTargetPackage =
+ jetpackConfig.getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleTargetPackage != null) {
+ gmsBuilder.setPubliclyVisibleTargetPackage(
+ new com.google.android.gms.appsearch.PackageIdentifier(
+ publiclyVisibleTargetPackage.getPackageName(),
+ publiclyVisibleTargetPackage.getSha256Certificate()));
+ }
+
+ return gmsBuilder.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 6d996e9..7391cbb 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,7 +27,9 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
import java.util.ArrayList;
import java.util.List;
@@ -115,14 +117,56 @@
}
/**
- * Creates a mock {@link VisibilityChecker}.
+ * Creates a mock {@link VisibilityChecker} where schema is searchable if prefixedSchema is
+ * one of the provided set of visiblePrefixedSchemas and caller does not have system access.
+ *
* @param visiblePrefixedSchemas Schema types that are accessible to any caller.
- * @return
+ * @return Mocked {@link VisibilityChecker} instance.
*/
@NonNull
public static VisibilityChecker createMockVisibilityChecker(
@NonNull Set<String> visiblePrefixedSchemas) {
- return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
- visiblePrefixedSchemas.contains(prefixedSchema);
+ return new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(
+ @NonNull CallerAccess callerAccess,
+ @NonNull String packageName,
+ @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return visiblePrefixedSchemas.contains(prefixedSchema);
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Creates a mock {@link VisibilityChecker}, where it can be configured if schema is searchable
+ * by caller and caller does not have system access.
+ *
+ * @param isSchemaSearchableByCaller Schema visibility for caller.
+ * @return Mocked {@link VisibilityChecker} instance.
+ */
+ @NonNull
+ public static VisibilityChecker createMockVisibilityChecker(
+ boolean isSchemaSearchableByCaller) {
+ return new VisibilityChecker() {
+ @Override
+ public boolean isSchemaSearchableByCaller(
+ @NonNull CallerAccess callerAccess,
+ @NonNull String packageName,
+ @NonNull String prefixedSchema,
+ @NonNull VisibilityStore visibilityStore) {
+ return isSchemaSearchableByCaller;
+ }
+
+ @Override
+ public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+ return false;
+ }
+ };
}
}
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
index 0c9464b..cc3f74c 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
@@ -25,6 +25,7 @@
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.stats.PutDocumentStats;
import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
import androidx.appsearch.localstorage.stats.SearchStats;
import androidx.appsearch.localstorage.stats.SetSchemaStats;
import androidx.appsearch.stats.SchemaMigrationStats;
@@ -63,6 +64,8 @@
/** Holds {@link androidx.appsearch.stats.SchemaMigrationStats} after logging. */
@Nullable
public SchemaMigrationStats mSchemaMigrationStats;
+ /** Holds {@link SearchSessionStats} after logging. */
+ @NonNull public List<SearchSessionStats> mSearchSessionsStats = new ArrayList<>();
@Override
public void logStats(@NonNull CallStats stats) {
@@ -103,4 +106,9 @@
public void logStats(@NonNull SchemaMigrationStats stats) {
mSchemaMigrationStats = stats;
}
+
+ @Override
+ public void logStats(@NonNull List<SearchSessionStats> searchSessionsStats) {
+ mSearchSessionsStats.addAll(searchSessionsStats);
+ }
}
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -35,6 +35,12 @@
method public abstract boolean required() default false;
}
+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+ method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+ method public abstract String name() default "";
+ method public abstract boolean required() default false;
+ }
+
@java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
}
@@ -115,11 +121,14 @@
field public static final int RESULT_NOT_FOUND = 6; // 0x6
field public static final int RESULT_OK = 0; // 0x0
field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+ field public static final int RESULT_RATE_LIMITED = 10; // 0xa
field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+ field public static final int RESULT_TIMED_OUT = 11; // 0xb
field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
}
public final class AppSearchSchema {
+ method public String getDescription();
method public java.util.List<java.lang.String!> getParentTypes();
method public java.util.List<androidx.appsearch.app.AppSearchSchema.PropertyConfig!> getProperties();
method public String getSchemaType();
@@ -132,6 +141,7 @@
ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
}
public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
method public androidx.appsearch.app.AppSearchSchema build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
}
public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
}
public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
}
@@ -174,6 +187,21 @@
ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+ }
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+ method public int getIndexingType();
+ field public static final int INDEXING_TYPE_NONE = 0; // 0x0
+ field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1
+ }
+
+ public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+ ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
}
public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
}
public abstract static class AppSearchSchema.PropertyConfig {
method public int getCardinality();
+ method public String getDescription();
method public String getName();
field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
}
public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
- method public boolean getDeletionPropagation();
method public int getIndexingType();
method public int getJoinableValueType();
method public int getTokenizerType();
@@ -217,7 +246,7 @@
ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
}
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+ ctor public EmbeddingVector(float[], String);
+ method public String getModelSignature();
+ method public float[] getValues();
+ }
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+ method 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 public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+ method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+ field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
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 JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+ field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+ field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
- field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+ field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+ field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+ field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+ field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+ field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+ field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+ field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
}
@@ -287,6 +338,8 @@
method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
method public double getPropertyDouble(String);
method public double[]? getPropertyDoubleArray(String);
+ method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+ method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
method public long getPropertyLong(String);
method public long[]? getPropertyLongArray(String);
method public java.util.Set<java.lang.String!> getPropertyNames();
@@ -301,6 +354,7 @@
}
public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
+ ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
ctor public GenericDocument.Builder(String, String, String);
method public androidx.appsearch.app.GenericDocument build();
method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
method public BuilderType setPropertyBytes(String, byte[]!...);
method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
method public BuilderType setPropertyDouble(String, double...);
+ method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
method public BuilderType setPropertyLong(String, long...);
method public BuilderType setPropertyString(String, java.lang.String!...);
method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
}
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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
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.SchemaVisibilityConfig!>!> getSchemaTypesVisibleToConfigs();
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();
@@ -348,7 +405,9 @@
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 setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
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 setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
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);
method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
public final class PutDocumentsRequest {
method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
+ method public java.util.List<androidx.appsearch.app.GenericDocument!> getTakenActionGenericDocuments();
}
public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument!>);
+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection<? extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.PutDocumentsRequest build();
}
@@ -472,11 +534,28 @@
method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
}
+ public final class SchemaVisibilityConfig {
+ method public java.util.List<androidx.appsearch.app.PackageIdentifier!> getAllowedPackages();
+ method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+ method public java.util.Set<java.util.Set<java.lang.Integer!>!> getRequiredPermissions();
+ }
+
+ public static final class SchemaVisibilityConfig.Builder {
+ ctor public SchemaVisibilityConfig.Builder();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set<java.lang.Integer!>);
+ method public androidx.appsearch.app.SchemaVisibilityConfig build();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+ }
+
public final class SearchResult {
method public String getDatabaseName();
method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
method public <T> T getDocument(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.GenericDocument getGenericDocument();
+ method public java.util.List<java.lang.Double!> getInformationalRankingSignals();
method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
method public String getPackageName();
@@ -485,6 +564,7 @@
public static final class SearchResult.Builder {
ctor public SearchResult.Builder(String, String);
+ method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
public final class SearchSpec {
method public String getAdvancedRankingExpression();
+ method public int getDefaultEmbeddingSearchMetricType();
method public java.util.List<java.lang.String!> getFilterNamespaces();
method public java.util.List<java.lang.String!> getFilterPackageNames();
+ method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
method public java.util.List<java.lang.String!> getFilterSchemas();
+ method public java.util.List<java.lang.String!> getInformationalRankingExpressions();
method public androidx.appsearch.app.JoinSpec? getJoinSpec();
method public int getMaxSnippetSize();
method public int getOrder();
@@ -540,18 +623,26 @@
method public int getResultCountPerPage();
method public int getResultGroupingLimit();
method public int getResultGroupingTypeFlags();
+ method public java.util.List<androidx.appsearch.app.EmbeddingVector!> getSearchEmbeddings();
+ method public String? getSearchSourceLogTag();
method public int getSnippetCount();
method public int getSnippetCountPerProperty();
method public int getTermMatch();
+ method public boolean isEmbeddingSearchEnabled();
+ method public boolean isListFilterHasPropertyFunctionEnabled();
method public boolean isListFilterQueryLanguageEnabled();
+ method public boolean isListFilterTokenizeFunctionEnabled();
method public boolean isNumericSearchEnabled();
method public boolean isVerbatimSearchEnabled();
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3
field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
field public static final int ORDER_ASCENDING = 1; // 0x1
field public static final int ORDER_DESCENDING = 0; // 0x0
- field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+ field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+ field public static final String SCHEMA_TYPE_WILDCARD = "*";
field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
field public static final int TERM_MATCH_PREFIX = 2; // 0x2
}
@@ -574,15 +666,27 @@
method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
method public androidx.appsearch.app.SearchSpec build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
public final class SearchSuggestionSpec {
method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterDocumentIds();
method public java.util.List<java.lang.String!> getFilterNamespaces();
+ method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
method public java.util.List<java.lang.String!> getFilterSchemas();
method public int getMaximumResultCount();
method public int getRankingStrategy();
@@ -629,6 +735,10 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
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.SchemaVisibilityConfig!>!> getSchemasVisibleToConfigs();
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();
@@ -653,26 +765,32 @@
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) 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<? extends java.lang.Object!>, 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
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.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) 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 clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class<? extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
}
- public class SetSchemaResponse {
+ public final class SetSchemaResponse {
method public java.util.Set<java.lang.String!> getDeletedTypes();
method public java.util.Set<java.lang.String!> getIncompatibleTypes();
method public java.util.Set<java.lang.String!> getMigratedTypes();
@@ -700,7 +818,7 @@
method public String getSchemaType();
}
- public class StorageInfo {
+ public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
method public long getSizeBytes();
@@ -771,6 +889,51 @@
}
+package androidx.appsearch.usagereporting {
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+ method public String? getQuery();
+ method public String? getReferencedQualifiedId();
+ method public int getResultRankGlobal();
+ method public int getResultRankInBlock();
+ method public long getTimeStayOnResultMillis();
+ }
+
+ @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+ ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+ ctor public ClickAction.Builder(String, String, long);
+ method public androidx.appsearch.usagereporting.ClickAction build();
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+ method public int getFetchedResultCount();
+ method public String? getQuery();
+ }
+
+ @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+ ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+ ctor public SearchAction.Builder(String, String, long);
+ method public androidx.appsearch.usagereporting.SearchAction build();
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+ method public long getActionTimestampMillis();
+ method public long getDocumentTtlMillis();
+ method public String getId();
+ method public String getNamespace();
+ }
+
+}
+
package androidx.appsearch.util {
public class DocumentIdUtil {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -35,6 +35,12 @@
method public abstract boolean required() default false;
}
+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+ method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+ method public abstract String name() default "";
+ method public abstract boolean required() default false;
+ }
+
@java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
}
@@ -115,11 +121,14 @@
field public static final int RESULT_NOT_FOUND = 6; // 0x6
field public static final int RESULT_OK = 0; // 0x0
field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+ field public static final int RESULT_RATE_LIMITED = 10; // 0xa
field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+ field public static final int RESULT_TIMED_OUT = 11; // 0xb
field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
}
public final class AppSearchSchema {
+ method public String getDescription();
method public java.util.List<java.lang.String!> getParentTypes();
method public java.util.List<androidx.appsearch.app.AppSearchSchema.PropertyConfig!> getProperties();
method public String getSchemaType();
@@ -132,6 +141,7 @@
ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
}
public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
method public androidx.appsearch.app.AppSearchSchema build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
}
public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
}
public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
}
@@ -174,6 +187,21 @@
ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+ }
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+ method public int getIndexingType();
+ field public static final int INDEXING_TYPE_NONE = 0; // 0x0
+ field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1
+ }
+
+ public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+ ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
}
public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
}
public abstract static class AppSearchSchema.PropertyConfig {
method public int getCardinality();
+ method public String getDescription();
method public String getName();
field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
}
public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
- method public boolean getDeletionPropagation();
method public int getIndexingType();
method public int getJoinableValueType();
method public int getTokenizerType();
@@ -217,7 +246,7 @@
ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
}
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+ ctor public EmbeddingVector(float[], String);
+ method public String getModelSignature();
+ method public float[] getValues();
+ }
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+ method 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 public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+ method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+ field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
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 JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+ field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+ field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
- field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+ field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+ field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+ field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+ field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+ field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+ field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+ field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
}
@@ -287,6 +338,8 @@
method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
method public double getPropertyDouble(String);
method public double[]? getPropertyDoubleArray(String);
+ method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+ method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
method public long getPropertyLong(String);
method public long[]? getPropertyLongArray(String);
method public java.util.Set<java.lang.String!> getPropertyNames();
@@ -301,6 +354,7 @@
}
public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
+ ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
ctor public GenericDocument.Builder(String, String, String);
method public androidx.appsearch.app.GenericDocument build();
method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
method public BuilderType setPropertyBytes(String, byte[]!...);
method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
method public BuilderType setPropertyDouble(String, double...);
+ method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
method public BuilderType setPropertyLong(String, long...);
method public BuilderType setPropertyString(String, java.lang.String!...);
method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
}
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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
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.SchemaVisibilityConfig!>!> getSchemaTypesVisibleToConfigs();
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();
@@ -348,7 +405,9 @@
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 setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
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 setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
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);
method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
public final class PutDocumentsRequest {
method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
+ method public java.util.List<androidx.appsearch.app.GenericDocument!> getTakenActionGenericDocuments();
}
public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument!>);
+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection<? extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.PutDocumentsRequest build();
}
@@ -472,11 +534,28 @@
method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
}
+ public final class SchemaVisibilityConfig {
+ method public java.util.List<androidx.appsearch.app.PackageIdentifier!> getAllowedPackages();
+ method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+ method public java.util.Set<java.util.Set<java.lang.Integer!>!> getRequiredPermissions();
+ }
+
+ public static final class SchemaVisibilityConfig.Builder {
+ ctor public SchemaVisibilityConfig.Builder();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set<java.lang.Integer!>);
+ method public androidx.appsearch.app.SchemaVisibilityConfig build();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+ }
+
public final class SearchResult {
method public String getDatabaseName();
method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
method public <T> T getDocument(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.GenericDocument getGenericDocument();
+ method public java.util.List<java.lang.Double!> getInformationalRankingSignals();
method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
method public String getPackageName();
@@ -485,6 +564,7 @@
public static final class SearchResult.Builder {
ctor public SearchResult.Builder(String, String);
+ method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
public final class SearchSpec {
method public String getAdvancedRankingExpression();
+ method public int getDefaultEmbeddingSearchMetricType();
method public java.util.List<java.lang.String!> getFilterNamespaces();
method public java.util.List<java.lang.String!> getFilterPackageNames();
+ method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
method public java.util.List<java.lang.String!> getFilterSchemas();
+ method public java.util.List<java.lang.String!> getInformationalRankingExpressions();
method public androidx.appsearch.app.JoinSpec? getJoinSpec();
method public int getMaxSnippetSize();
method public int getOrder();
@@ -540,18 +623,26 @@
method public int getResultCountPerPage();
method public int getResultGroupingLimit();
method public int getResultGroupingTypeFlags();
+ method public java.util.List<androidx.appsearch.app.EmbeddingVector!> getSearchEmbeddings();
+ method public String? getSearchSourceLogTag();
method public int getSnippetCount();
method public int getSnippetCountPerProperty();
method public int getTermMatch();
+ method public boolean isEmbeddingSearchEnabled();
+ method public boolean isListFilterHasPropertyFunctionEnabled();
method public boolean isListFilterQueryLanguageEnabled();
+ method public boolean isListFilterTokenizeFunctionEnabled();
method public boolean isNumericSearchEnabled();
method public boolean isVerbatimSearchEnabled();
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3
field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
field public static final int ORDER_ASCENDING = 1; // 0x1
field public static final int ORDER_DESCENDING = 0; // 0x0
- field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+ field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+ field public static final String SCHEMA_TYPE_WILDCARD = "*";
field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
field public static final int TERM_MATCH_PREFIX = 2; // 0x2
}
@@ -574,15 +666,27 @@
method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
method public androidx.appsearch.app.SearchSpec build();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
public final class SearchSuggestionSpec {
method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterDocumentIds();
method public java.util.List<java.lang.String!> getFilterNamespaces();
+ method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
method public java.util.List<java.lang.String!> getFilterSchemas();
method public int getMaximumResultCount();
method public int getRankingStrategy();
@@ -629,6 +735,10 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
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.SchemaVisibilityConfig!>!> getSchemasVisibleToConfigs();
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();
@@ -653,26 +765,32 @@
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) 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<? extends java.lang.Object!>, 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
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.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) 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 clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class<? extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
}
- public class SetSchemaResponse {
+ public final class SetSchemaResponse {
method public java.util.Set<java.lang.String!> getDeletedTypes();
method public java.util.Set<java.lang.String!> getIncompatibleTypes();
method public java.util.Set<java.lang.String!> getMigratedTypes();
@@ -700,7 +818,7 @@
method public String getSchemaType();
}
- public class StorageInfo {
+ public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
method public long getSizeBytes();
@@ -771,6 +889,51 @@
}
+package androidx.appsearch.usagereporting {
+
+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+ method public String? getQuery();
+ method public String? getReferencedQualifiedId();
+ method public int getResultRankGlobal();
+ method public int getResultRankInBlock();
+ method public long getTimeStayOnResultMillis();
+ }
+
+ @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+ ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+ ctor public ClickAction.Builder(String, String, long);
+ method public androidx.appsearch.usagereporting.ClickAction build();
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+ method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+ method public int getFetchedResultCount();
+ method public String? getQuery();
+ }
+
+ @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+ ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+ ctor public SearchAction.Builder(String, String, long);
+ method public androidx.appsearch.usagereporting.SearchAction build();
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+ method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+ method public long getActionTimestampMillis();
+ method public long getDocumentTtlMillis();
+ method public String getId();
+ method public String getNamespace();
+ }
+
+}
+
package androidx.appsearch.util {
public class DocumentIdUtil {
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 8d67baa..6e8ddd6 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -44,6 +44,8 @@
implementation('androidx.concurrent:concurrent-futures:1.0.0')
implementation("androidx.core:core:1.6.0")
+ annotationProcessor project(':appsearch:appsearch-compiler')
+
androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
androidTestImplementation project(':appsearch:appsearch-builtin-types')
androidTestImplementation project(':appsearch:appsearch-local-storage')
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 5d52268..7d18f74 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -24,6 +24,7 @@
import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
+import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
import static com.google.common.truth.Truth.assertThat;
@@ -2764,4 +2765,160 @@
assertThat(person.getFirstName()).isEqualTo("first");
assertThat(person.getLastName()).isEqualTo("last");
}
+
+ @Document
+ static class EmailWithEmbedding {
+ @Document.Namespace
+ String mNamespace;
+
+ @Document.Id
+ String mId;
+
+ @Document.CreationTimestampMillis
+ long mCreationTimestampMillis;
+
+ @Document.StringProperty
+ String mSender;
+
+ // Default non-indexable embedding
+ @Document.EmbeddingProperty
+ EmbeddingVector mSenderEmbedding;
+
+ @Document.EmbeddingProperty(indexingType = 1)
+ EmbeddingVector mTitleEmbedding;
+
+ @Document.EmbeddingProperty(indexingType = 1)
+ Collection<EmbeddingVector> mReceiverEmbeddings;
+
+ @Document.EmbeddingProperty(indexingType = 1)
+ EmbeddingVector[] mBodyEmbeddings;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ EmailWithEmbedding email = (EmailWithEmbedding) o;
+ return Objects.equals(mNamespace, email.mNamespace) && Objects.equals(mId,
+ email.mId) && Objects.equals(mSender, email.mSender)
+ && Objects.equals(mSenderEmbedding, email.mSenderEmbedding)
+ && Objects.equals(mTitleEmbedding, email.mTitleEmbedding)
+ && Objects.equals(mReceiverEmbeddings, email.mReceiverEmbeddings)
+ && Arrays.equals(mBodyEmbeddings, email.mBodyEmbeddings);
+ }
+
+ public static EmailWithEmbedding createSampleDoc() {
+ EmbeddingVector embedding1 =
+ new EmbeddingVector(new float[]{1, 2, 3}, "model1");
+ EmbeddingVector embedding2 =
+ new EmbeddingVector(new float[]{-1, -2, -3}, "model2");
+ EmbeddingVector embedding3 =
+ new EmbeddingVector(new float[]{0.1f, 0.2f, 0.3f, 0.4f}, "model3");
+ EmbeddingVector embedding4 =
+ new EmbeddingVector(new float[]{-0.1f, -0.2f, -0.3f, -0.4f}, "model3");
+ EmailWithEmbedding email = new EmailWithEmbedding();
+ email.mNamespace = "namespace";
+ email.mId = "id";
+ email.mCreationTimestampMillis = 1000;
+ email.mSender = "sender";
+ email.mSenderEmbedding = embedding1;
+ email.mTitleEmbedding = embedding2;
+ email.mReceiverEmbeddings = Collections.singletonList(embedding3);
+ email.mBodyEmbeddings = new EmbeddingVector[]{embedding3, embedding4};
+ return email;
+ }
+ }
+
+ @Test
+ public void testEmbeddingGenericDocumentConversion() throws Exception {
+ EmailWithEmbedding inEmail = EmailWithEmbedding.createSampleDoc();
+ GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inEmail);
+ GenericDocument genericDocument2 = GenericDocument.fromDocumentClass(inEmail);
+ EmailWithEmbedding outEmail = genericDocument2.toDocumentClass(EmailWithEmbedding.class);
+
+ assertThat(inEmail).isNotSameInstanceAs(outEmail);
+ assertThat(inEmail).isEqualTo(outEmail);
+ assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2);
+ assertThat(genericDocument1).isEqualTo(genericDocument2);
+ }
+
+ @Test
+ public void testEmbeddingSearch() throws Exception {
+ assumeTrue(mSession.getFeatures().isFeatureSupported(
+ Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addDocumentClasses(EmailWithEmbedding.class)
+ .build()).get();
+
+ // Create and add a document
+ EmailWithEmbedding email = EmailWithEmbedding.createSampleDoc();
+ checkIsBatchResultSuccess(mSession.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addDocuments(email)
+ .build()));
+
+ // An empty query should retrieve this document.
+ SearchResults searchResults = mSession.search("",
+ new SearchSpec.Builder().build());
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(1);
+ // Convert GenericDocument to EmailWithEmbedding and check values.
+ EmailWithEmbedding outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+ assertThat(outputDocument).isEqualTo(email);
+
+ // senderEmbedding is non-indexable, so querying for it will return nothing.
+ searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+ new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+ .addSearchEmbeddings(email.mSenderEmbedding)
+ .setRankingStrategy(
+ "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build());
+ results = retrieveAllSearchResults(searchResults);
+ assertThat(results).isEmpty();
+
+ // titleEmbedding is indexable, and querying for it using itself will return a cosine
+ // similarity score of 1.
+ searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+ new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+ .addSearchEmbeddings(email.mTitleEmbedding)
+ .setRankingStrategy(
+ "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build());
+ results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(1);
+ // Convert GenericDocument to EmailWithEmbedding and check values.
+ outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+ assertThat(outputDocument).isEqualTo(email);
+
+ // Both receiverEmbeddings and bodyEmbeddings are indexable, and in this specific
+ // document, they together hold three embedding vectors with the same signature.
+ searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)",
+ new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+ // Using one of the three vectors to query
+ .addSearchEmbeddings(email.mBodyEmbeddings[0])
+ .setRankingStrategy(
+ // We should get a score of 3 for "len", since there are three
+ // embedding vectors matched.
+ "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build());
+ results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+ // Convert GenericDocument to EmailWithEmbedding and check values.
+ outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+ assertThat(outputDocument).isEqualTo(email);
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java
deleted file mode 100644
index 686c44f..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright 2023 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 com.google.common.truth.Truth.assertThat;
-
-import androidx.appsearch.testutil.AppSearchEmail;
-
-import org.junit.Test;
-
-import java.util.List;
-
-/** Tests for private APIs of {@link AppSearchSchema}. */
-public class AppSearchSchemaInternalTest {
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testParentTypes() {
- AppSearchSchema schema =
- new AppSearchSchema.Builder("EmailMessage")
- .addParentType("Email")
- .addParentType("Message")
- .build();
- assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testDuplicateParentTypes() {
- AppSearchSchema schema =
- new AppSearchSchema.Builder("EmailMessage")
- .addParentType("Email")
- .addParentType("Message")
- .addParentType("Email")
- .build();
- assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {
- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
- .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")
- .build();
- assertThat(documentPropertyConfig.getIndexableNestedProperties())
- .containsExactly("prop1", "prop2", "prop1.prop2");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {
- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
- .addIndexableNestedPropertyPaths(
- new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
- .build();
- assertThat(documentPropertyConfig.getIndexableNestedProperties())
- .containsExactly("prop1", "prop1.prop2");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {
- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
- .addIndexableNestedPropertyPaths(
- new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
- .addIndexableNestedProperties("prop1")
- .build();
- assertThat(documentPropertyConfig.getIndexableNestedProperties())
- .containsExactly("prop1", "prop1.prop2");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {
- AppSearchSchema.DocumentPropertyConfig.Builder builder =
- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
- .addIndexableNestedProperties("prop1");
- AppSearchSchema.DocumentPropertyConfig config1 = builder.build();
- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
- builder.addIndexableNestedProperties("prop2");
- AppSearchSchema.DocumentPropertyConfig config2 = builder.build();
- assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
- builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));
- AppSearchSchema.DocumentPropertyConfig config3 = builder.build();
- assertThat(config3.getIndexableNestedProperties())
- .containsExactly("prop1", "prop2", "prop3");
- assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testPropertyConfig() {
- AppSearchSchema schema =
- new AppSearchSchema.Builder("Test")
- .addProperty(
- new AppSearchSchema.StringPropertyConfig.Builder("string")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- AppSearchSchema.StringPropertyConfig
- .INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- AppSearchSchema.StringPropertyConfig
- .TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(
- new AppSearchSchema.LongPropertyConfig.Builder("long")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(
- AppSearchSchema.LongPropertyConfig
- .INDEXING_TYPE_NONE)
- .build())
- .addProperty(
- new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(
- AppSearchSchema.LongPropertyConfig
- .INDEXING_TYPE_RANGE)
- .build())
- .addProperty(
- new AppSearchSchema.DoublePropertyConfig.Builder("double")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
- .build())
- .addProperty(
- new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
- .build())
- .addProperty(
- new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .build())
- .addProperty(
- new AppSearchSchema.DocumentPropertyConfig.Builder(
- "document1", AppSearchEmail.SCHEMA_TYPE)
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
- .setShouldIndexNestedProperties(true)
- .build())
- .addProperty(
- new AppSearchSchema.DocumentPropertyConfig.Builder(
- "document2", AppSearchEmail.SCHEMA_TYPE)
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
- .setShouldIndexNestedProperties(false)
- .addIndexableNestedProperties("path1", "path2", "path3")
- .build())
- .addProperty(
- new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .setJoinableValueType(
- AppSearchSchema.StringPropertyConfig
- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .build())
- .addProperty(
- new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
- .setJoinableValueType(
- AppSearchSchema.StringPropertyConfig
- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .build())
- .build();
-
- assertThat(schema.getSchemaType()).isEqualTo("Test");
- List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
- assertThat(properties).hasSize(10);
-
- assertThat(properties.get(0).getName()).isEqualTo("string");
- assertThat(properties.get(0).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
- assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
- .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
- assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
- .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
-
- assertThat(properties.get(1).getName()).isEqualTo("long");
- assertThat(properties.get(1).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
- assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
- .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);
-
- assertThat(properties.get(2).getName()).isEqualTo("indexableLong");
- assertThat(properties.get(2).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
- assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())
- .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
-
- assertThat(properties.get(3).getName()).isEqualTo("double");
- assertThat(properties.get(3).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
- assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
-
- assertThat(properties.get(4).getName()).isEqualTo("boolean");
- assertThat(properties.get(4).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
- assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
-
- assertThat(properties.get(5).getName()).isEqualTo("bytes");
- assertThat(properties.get(5).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
- assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
-
- assertThat(properties.get(6).getName()).isEqualTo("document1");
- assertThat(properties.get(6).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
- assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())
- .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
- assertThat(
- ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))
- .shouldIndexNestedProperties())
- .isEqualTo(true);
-
- assertThat(properties.get(7).getName()).isEqualTo("document2");
- assertThat(properties.get(7).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
- assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())
- .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
- assertThat(
- ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
- .shouldIndexNestedProperties())
- .isEqualTo(false);
- assertThat(
- ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
- .getIndexableNestedProperties())
- .containsExactly("path1", "path2", "path3");
-
- assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");
- assertThat(properties.get(8).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
- assertThat(
- ((AppSearchSchema.StringPropertyConfig) properties.get(8))
- .getJoinableValueType())
- .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-
- assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");
- assertThat(properties.get(9).getCardinality())
- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
- assertThat(
- ((AppSearchSchema.StringPropertyConfig) properties.get(9))
- .getJoinableValueType())
- .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
- }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index c984f92..b72cf54 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -21,16 +21,12 @@
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 androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
import androidx.appsearch.testutil.AppSearchEmail;
-import androidx.appsearch.util.DocumentIdUtil;
-import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
@@ -46,6 +42,8 @@
import java.util.Set;
import java.util.concurrent.ExecutorService;
+
+/** This class holds all tests that won't be exported to the framework. */
public abstract class AppSearchSessionInternalTestBase {
static final String DB_NAME_1 = "";
@@ -77,730 +75,10 @@
mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
- // TODO(b/228240987) delete this test when we support property restrict for multiple terms
- @Test
- public void testSearchSuggestion_propertyFilter() throws Exception {
- // Schema registration
- AppSearchSchema schemaType1 =
- new AppSearchSchema.Builder("Type1")
- .addProperty(
- new StringPropertyConfig.Builder("propertyone")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .addProperty(
- new StringPropertyConfig.Builder("propertytwo")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .build();
- AppSearchSchema schemaType2 =
- new AppSearchSchema.Builder("Type2")
- .addProperty(
- new StringPropertyConfig.Builder("propertythree")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .addProperty(
- new StringPropertyConfig.Builder("propertyfour")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .build();
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
- .get();
-
- // Index documents
- GenericDocument doc1 =
- new GenericDocument.Builder<>("namespace", "id1", "Type1")
- .setPropertyString("propertyone", "termone")
- .setPropertyString("propertytwo", "termtwo")
- .build();
- GenericDocument doc2 =
- new GenericDocument.Builder<>("namespace", "id2", "Type2")
- .setPropertyString("propertythree", "termthree")
- .setPropertyString("propertyfour", "termfour")
- .build();
-
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
-
- SearchSuggestionResult resultOne =
- new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
- SearchSuggestionResult resultTwo =
- new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
- SearchSuggestionResult resultThree =
- new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
- SearchSuggestionResult resultFour =
- new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
-
- // Only search for type1/propertyone
- List<SearchSuggestionResult> suggestions =
- mDb1.searchSuggestionAsync(
- /* suggestionQueryExpression= */ "t",
- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
- .addFilterSchemas("Type1")
- .addFilterProperties(
- "Type1", ImmutableList.of("propertyone"))
- .build())
- .get();
- assertThat(suggestions).containsExactly(resultOne);
-
- // Only search for type1/propertyone and type1/propertytwo
- suggestions =
- mDb1.searchSuggestionAsync(
- /* suggestionQueryExpression= */ "t",
- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
- .addFilterSchemas("Type1")
- .addFilterProperties(
- "Type1",
- ImmutableList.of("propertyone", "propertytwo"))
- .build())
- .get();
- assertThat(suggestions).containsExactly(resultOne, resultTwo);
-
- // Only search for type1/propertyone and type2/propertythree
- suggestions =
- mDb1.searchSuggestionAsync(
- /* suggestionQueryExpression= */ "t",
- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
- .addFilterSchemas("Type1", "Type2")
- .addFilterProperties(
- "Type1", ImmutableList.of("propertyone"))
- .addFilterProperties(
- "Type2", ImmutableList.of("propertythree"))
- .build())
- .get();
- assertThat(suggestions).containsExactly(resultOne, resultThree);
-
- // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
- suggestions =
- mDb1.searchSuggestionAsync(
- /* suggestionQueryExpression= */ "t",
- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
- .addFilterSchemas("Type1", "Type2")
- .addFilterProperties(
- "Type1", ImmutableList.of("propertyone"))
- .addFilterPropertyPaths(
- "Type2",
- ImmutableList.of(new PropertyPath("propertyfour")))
- .build())
- .get();
- assertThat(suggestions).containsExactly(resultOne, resultFour);
-
- // Only search for type1/propertyone and everything in type2
- suggestions =
- mDb1.searchSuggestionAsync(
- /* suggestionQueryExpression= */ "t",
- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
- .addFilterProperties(
- "Type1", ImmutableList.of("propertyone"))
- .build())
- .get();
- assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFilters() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .build()).get();
-
- // Index two documents
- AppSearchEmail email1 =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- AppSearchEmail email2 =
- new AppSearchEmail.Builder("namespace", "id2")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example subject with some body")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email1, email2).build()));
-
- // Query with type property filters {"Email", ["subject", "to"]}
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // Only email2 should be returned because email1 doesn't have the term "body" in subject
- // or to fields
- assertThat(documents).containsExactly(email2);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("Note")
- .addProperty(new StringPropertyConfig.Builder("title")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .build())
- .build()).get();
-
- // Index two documents
- AppSearchEmail email =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- GenericDocument note =
- new GenericDocument.Builder<>("namespace", "id2", "Note")
- .setCreationTimestampMillis(1000)
- .setPropertyString("title", "Note title")
- .setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email, note).build()));
-
- // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
- // schema has body in its property filter but Email schema doesn't.
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
- .addFilterProperties("Note", ImmutableList.of("body"))
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // Only the note document should be returned because the email property filter doesn't
- // allow searching in the body.
- assertThat(documents).containsExactly(note);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("Note")
- .addProperty(new StringPropertyConfig.Builder("title")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .build())
- .build()).get();
-
- // Index two documents
- AppSearchEmail email =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example subject with some body")
- .setBody("This is the body of the testPut email")
- .build();
- GenericDocument note =
- new GenericDocument.Builder<>("namespace", "id2", "Note")
- .setCreationTimestampMillis(1000)
- .setPropertyString("title", "Note title")
- .setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email, note).build()));
-
- // Query with type property paths {"*": ["subject", "title"]}
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
- ImmutableList.of("subject", "title"))
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The wildcard property filter will apply to both the Email and Note schema. The email
- // document should be returned since it has the term "body" in its subject property. The
- // note document should not be returned since it doesn't have the term "body" in the title
- // property (subject property is not applicable for Note schema)
- assertThat(documents).containsExactly(email);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("Note")
- .addProperty(new StringPropertyConfig.Builder("title")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .build())
- .build()).get();
-
- // Index two documents
- AppSearchEmail email =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example subject with some body")
- .setBody("This is the body of the testPut email")
- .build();
- GenericDocument note =
- new GenericDocument.Builder<>("namespace", "id2", "Note")
- .setCreationTimestampMillis(1000)
- .setPropertyString("title", "Note title")
- .setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email, note).build()));
-
- // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
- ImmutableList.of("subject", "title"))
- .addFilterProperties("Note", ImmutableList.of("body"))
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The wildcard property filter will only apply to the Email schema since Note schema has
- // its own explicit property filter specified. The email document should be returned since
- // it has the term "body" in its subject property. The note document should also be returned
- // since it has the term "body" in the body property.
- assertThat(documents).containsExactly(email, note);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("Note")
- .addProperty(new StringPropertyConfig.Builder("title")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .build())
- .build()).get();
-
- // Index two documents
- AppSearchEmail email =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example subject with some body")
- .setBody("This is the body of the testPut email")
- .build();
- GenericDocument note =
- new GenericDocument.Builder<>("namespace", "id2", "Note")
- .setCreationTimestampMillis(1000)
- .setPropertyString("title", "Note title")
- .setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email, note).build()));
-
- // Query with type property paths {"NonExistentType": ["to", "title"]}
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The supplied property filters don't apply to either schema types. Both the documents
- // should be returned since the term "body" is present in at least one of their properties.
- assertThat(documents).containsExactly(email, note);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersEmpty() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("Note")
- .addProperty(new StringPropertyConfig.Builder("title")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(
- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .build())
- .build()).get();
-
- // Index two documents
- AppSearchEmail email =
- new AppSearchEmail.Builder("namespace", "id1")
- .setCreationTimestampMillis(1000)
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- GenericDocument note =
- new GenericDocument.Builder<>("namespace", "id2", "Note")
- .setCreationTimestampMillis(1000)
- .setPropertyString("title", "Note title")
- .setPropertyString("body", "Note body").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder()
- .addGenericDocuments(email, note).build()));
-
- // Query with type property paths {"email": []}
- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The email document should not be returned since the property filter doesn't allow
- // searching any property.
- assertThat(documents).containsExactly(note);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- assumeTrue(mDb1.getFeatures()
- .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
- // A full example of how join might be used with property filters in join spec
- AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
- .addProperty(new StringPropertyConfig.Builder("entityId")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setJoinableValueType(StringPropertyConfig
- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).addProperty(new StringPropertyConfig.Builder("note")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).addProperty(new StringPropertyConfig.Builder("viewType")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).build();
-
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
- .build()).get();
-
- // Index 2 email documents
- AppSearchEmail inEmail =
- new AppSearchEmail.Builder("namespace", "id1")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
-
- AppSearchEmail inEmail2 =
- new AppSearchEmail.Builder("namespace", "id2")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
-
- // Index 2 viewAction documents, one for email1 and the other for email2
- String qualifiedId1 =
- DocumentIdUtil.createQualifiedId(
- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
- "namespace", "id1");
- String qualifiedId2 =
- DocumentIdUtil.createQualifiedId(
- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
- "namespace", "id2");
- GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
- .setPropertyString("entityId", qualifiedId1)
- .setPropertyString("note", "Viewed email on Monday")
- .setPropertyString("viewType", "Stared").build();
- GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
- .setPropertyString("entityId", qualifiedId2)
- .setPropertyString("note", "Viewed email on Tuesday")
- .setPropertyString("viewType", "Viewed").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
- viewAction1, viewAction2)
- .build()));
-
- // The nested search spec only allows searching the viewType property for viewAction
- // schema type. It also specifies a property filter for Email schema.
- SearchSpec nestedSearchSpec =
- new SearchSpec.Builder()
- .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
- ImmutableList.of("subject"))
- .build();
-
- // Search for the term "Viewed" in join spec
- JoinSpec js = new JoinSpec.Builder("entityId")
- .setNestedSearch("Viewed", nestedSearchSpec)
- .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
- .build();
-
- SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
- .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
- .setJoinSpec(js)
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .build());
-
- List<SearchResult> sr = searchResults.getNextPageAsync().get();
-
- // Both email docs are returned, email2 comes first because it has higher number of
- // joined documents. The property filters for Email schema specified in the nested search
- // specs don't apply to the outer query (otherwise none of the email documents would have
- // been returned).
- assertThat(sr).hasSize(2);
-
- // Email2 has a viewAction document viewAction2 that satisfies the property filters in
- // the join spec, so it should be present in the joined results.
- assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
- assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
- assertThat(sr.get(0).getJoinedResults()).hasSize(1);
- assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
- // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
- // in the join spec, so it should not be present in the joined results.
- assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
- assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
- assertThat(sr.get(1).getJoinedResults()).isEmpty();
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- assumeTrue(mDb1.getFeatures()
- .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
- // A full example of how join might be used with property filters in join spec
- AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
- .addProperty(new StringPropertyConfig.Builder("entityId")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setJoinableValueType(StringPropertyConfig
- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).addProperty(new StringPropertyConfig.Builder("note")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).addProperty(new StringPropertyConfig.Builder("viewType")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build()
- ).build();
-
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
- .build()).get();
-
- // Index 2 email documents
- AppSearchEmail inEmail =
- new AppSearchEmail.Builder("namespace", "id1")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
-
- AppSearchEmail inEmail2 =
- new AppSearchEmail.Builder("namespace", "id2")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
-
- // Index 2 viewAction documents, one for email1 and the other for email2
- String qualifiedId1 =
- DocumentIdUtil.createQualifiedId(
- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
- "namespace", "id1");
- String qualifiedId2 =
- DocumentIdUtil.createQualifiedId(
- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
- "namespace", "id2");
- GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
- .setPropertyString("entityId", qualifiedId1)
- .setPropertyString("note", "Viewed email on Monday")
- .setPropertyString("viewType", "Stared").build();
- GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
- .setPropertyString("entityId", qualifiedId2)
- .setPropertyString("note", "Viewed email on Tuesday")
- .setPropertyString("viewType", "Viewed").build();
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
- viewAction1, viewAction2)
- .build()));
-
- // The nested search spec doesn't specify any property filters.
- SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
-
- // Search for the term "Viewed" in join spec
- JoinSpec js = new JoinSpec.Builder("entityId")
- .setNestedSearch("Viewed", nestedSearchSpec)
- .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
- .build();
-
- // Outer search spec adds property filters for both Email and ViewAction schema
- SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
- .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
- .setJoinSpec(js)
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
- .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
- .build());
-
- List<SearchResult> sr = searchResults.getNextPageAsync().get();
-
- // Both email docs are returned as they both satisfy the property filters for Email, email2
- // comes first because it has higher id lexicographically.
- assertThat(sr).hasSize(2);
-
- // Email2 has a viewAction document viewAction2 that satisfies the property filters in
- // the outer spec (although those property filters are irrelevant for joined documents),
- // it should be present in the joined results.
- assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
- assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
- assertThat(sr.get(0).getJoinedResults()).hasSize(1);
- assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
- // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
- // in the outer spec, but property filters in the outer spec should not apply on joined
- // documents, so viewAction1 should be present in the joined results.
- assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
- assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
- assertThat(sr.get(0).getJoinedResults()).hasSize(1);
- assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
- }
-
- // TODO(b/296088047): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_typePropertyFiltersNotSupported() throws Exception {
- assumeFalse(mDb1.getFeatures().isFeatureSupported(
- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .build()).get();
-
- // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
- // exception is thrown
- SearchSpec searchSpec = new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
- .build();
- UnsupportedOperationException exception =
- assertThrows(UnsupportedOperationException.class,
- () -> mDb1.search("body", searchSpec));
- assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
- + " is not available on this AppSearch implementation.");
- }
-
// TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
@Test
public void testGetSchema_joinableValueType() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
- assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
AppSearchSchema inSchema =
new AppSearchSchema.Builder("Test")
.addProperty(
@@ -820,11 +98,6 @@
.setJoinableValueType(
StringPropertyConfig
.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- // TODO(b/274157614): Export this to framework when we
- // can access hidden APIs.
- // @exportToFramework:startStrip()
- .setDeletionPropagation(true)
- // @exportToFramework:endStrip()
.build())
.build();
@@ -837,465 +110,6 @@
assertThat(actual).containsExactlyElementsIn(request.getSchemas());
}
- // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
- @Test
- public void testGetSchema_deletionPropagation_unsupported() {
- assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
- assumeFalse(
- mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
- AppSearchSchema schema =
- new AppSearchSchema.Builder("Test")
- .addProperty(
- new StringPropertyConfig.Builder("qualifiedIdDeletionPropagation")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setJoinableValueType(
- StringPropertyConfig
- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .setDeletionPropagation(true)
- .build())
- .build();
- SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();
- Exception e =
- assertThrows(
- UnsupportedOperationException.class,
- () -> mDb1.setSchemaAsync(request).get());
- assertThat(e.getMessage())
- .isEqualTo(
- "Setting deletion propagation is not supported "
- + "on this AppSearch implementation.");
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
- assumeTrue(
- mDb1.getFeatures()
- .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
- // Schema registration
- AppSearchSchema genericSchema =
- new AppSearchSchema.Builder("Generic")
- .addProperty(
- new StringPropertyConfig.Builder("foo")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(
- StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .build();
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(genericSchema)
- .build())
- .get();
-
- // Index four documents.
- AppSearchEmail inEmail1 =
- new AppSearchEmail.Builder("namespace1", "id1")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
- AppSearchEmail inEmail2 =
- new AppSearchEmail.Builder("namespace1", "id2")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
- AppSearchEmail inEmail3 =
- new AppSearchEmail.Builder("namespace2", "id3")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
- AppSearchEmail inEmail4 =
- new AppSearchEmail.Builder("namespace2", "id4")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
- AppSearchEmail inEmail5 =
- new AppSearchEmail.Builder("namespace2", "id5")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
- GenericDocument inDoc1 =
- new GenericDocument.Builder<>("namespace3", "id6", "Generic")
- .setPropertyString("foo", "body")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
- GenericDocument inDoc2 =
- new GenericDocument.Builder<>("namespace3", "id7", "Generic")
- .setPropertyString("foo", "body")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
- GenericDocument inDoc3 =
- new GenericDocument.Builder<>("namespace4", "id8", "Generic")
- .setPropertyString("foo", "body")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
-
- // Query with per package result grouping. Only the last document 'doc3' should be
- // returned.
- SearchResults searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* resultLimit= */ 1)
- .build());
- List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3);
-
- // Query with per namespace result grouping. Only the last document in each namespace should
- // be returned ('doc3', 'doc2', 'email5' and 'email2').
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
- /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
- // Query with per namespace result grouping. Two of the last documents in each namespace
- // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
- /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents)
- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
- // Query with per schema result grouping. Only the last document of each schema type should
- // be returned ('doc3', 'email5')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inEmail5);
-
- // Query with per schema result grouping. Only the last two documents of each schema type
- // should be returned ('doc3', 'doc2', 'email5', 'email4')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
- // Query with per package and per namespace result grouping. Only the last document in each
- // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
- // Query with per package and per namespace result grouping. Only the last two documents
- // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
- // 'email2', 'email1')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents)
- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
- // Query with per package and per schema type result grouping. Only the last document in
- // each schema type should be returned. ('doc3', 'email5')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_SCHEMA
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inEmail5);
-
- // Query with per package and per schema type result grouping. Only the last two document in
- // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_SCHEMA
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
- // Query with per namespace and per schema type result grouping. Only the last document in
- // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
- /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
- // Query with per namespace and per schema type result grouping. Only the last two documents
- // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
- // 'email2', 'email1')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
- /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents)
- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
- // Query with per namespace, per package and per schema type result grouping. Only the last
- // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 1)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
- // Query with per namespace, per package and per schema type result grouping. Only the last
- // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
- // 'email4', 'email2', 'email1')
- searchResults =
- mDb1.search(
- "body",
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 2)
- .build());
- documents = convertSearchResultsToDocuments(searchResults);
- assertThat(documents)
- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
- }
-
- // TODO(b/291122592): move to CTS once the APIs it uses are public
- @Test
- public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
- assumeFalse(
- mDb1.getFeatures()
- .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
- // Schema registration
- mDb1.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
- .get();
-
- // Index four documents.
- AppSearchEmail inEmail1 =
- new AppSearchEmail.Builder("namespace1", "id1")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
- AppSearchEmail inEmail2 =
- new AppSearchEmail.Builder("namespace1", "id2")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
- AppSearchEmail inEmail3 =
- new AppSearchEmail.Builder("namespace2", "id3")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
- AppSearchEmail inEmail4 =
- new AppSearchEmail.Builder("namespace2", "id4")
- .setFrom("[email protected]")
- .setTo("[email protected]", "[email protected]")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
- checkIsBatchResultSuccess(
- mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
-
- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
- // UnsupportedOperationException will be thrown.
- SearchSpec searchSpec1 =
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
- .build();
- UnsupportedOperationException exception =
- assertThrows(
- UnsupportedOperationException.class,
- () -> mDb1.search("body", searchSpec1));
- assertThat(exception)
- .hasMessageThat()
- .contains(
- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
- + " is not available on this"
- + " AppSearch implementation.");
-
- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
- // UnsupportedOperationException will be thrown.
- SearchSpec searchSpec2 =
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_PACKAGE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
- /* resultLimit= */ 1)
- .build();
- exception =
- assertThrows(
- UnsupportedOperationException.class,
- () -> mDb1.search("body", searchSpec2));
- assertThat(exception)
- .hasMessageThat()
- .contains(
- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
- + " is not available on this"
- + " AppSearch implementation.");
-
- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
- // UnsupportedOperationException will be thrown.
- SearchSpec searchSpec3 =
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
- /* resultLimit= */ 1)
- .build();
- exception =
- assertThrows(
- UnsupportedOperationException.class,
- () -> mDb1.search("body", searchSpec3));
- assertThat(exception)
- .hasMessageThat()
- .contains(
- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
- + " is not available on this"
- + " AppSearch implementation.");
-
- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
- // UnsupportedOperationException will be thrown.
- SearchSpec searchSpec4 =
- new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_SCHEMA
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
- /* resultLimit= */ 1)
- .build();
- exception =
- assertThrows(
- UnsupportedOperationException.class,
- () -> mDb1.search("body", searchSpec4));
- assertThat(exception)
- .hasMessageThat()
- .contains(
- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
- + " is not available on this"
- + " AppSearch implementation.");
- }
-
@Test
public void testQuery_typeFilterWithPolymorphism() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
@@ -1496,6 +310,113 @@
}
@Test
+ public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+ // Schema registration
+ AppSearchSchema personSchema =
+ new AppSearchSchema.Builder("Person")
+ .addProperty(
+ new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("emailAddress")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ AppSearchSchema artistSchema =
+ new AppSearchSchema.Builder("Artist")
+ .addParentType("Person")
+ .addProperty(
+ new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("emailAddress")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("company")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(personSchema)
+ .addSchemas(artistSchema)
+ .build())
+ .get();
+
+ // Index two documents
+ GenericDocument personDoc =
+ new GenericDocument.Builder<>("namespace", "id1", "Person")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("name", "Foo Person")
+ .setPropertyString("emailAddress", "[email protected]")
+ .build();
+ GenericDocument artistDoc =
+ new GenericDocument.Builder<>("namespace", "id2", "Artist")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("name", "Foo Artist")
+ .setPropertyString("emailAddress", "[email protected]")
+ .setPropertyString("company", "Company")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(personDoc, artistDoc)
+ .build()));
+
+ // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
+ // a schema filter for the "Person".
+ // This will be expanded to paths {"Person", ["name"]} and
+ // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
+ // polymorphism.
+ SearchResults searchResults =
+ mDb1.search(
+ "Foo",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterSchemas("Person")
+ .addProjection("Person", ImmutableList.of("name"))
+ .addProjection("Artist", ImmutableList.of("emailAddress"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // The person document should have been returned with only the "name" property. The artist
+ // document should have been returned with all of its properties.
+ GenericDocument expectedPerson =
+ new GenericDocument.Builder<>("namespace", "id1", "Person")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("name", "Foo Person")
+ .build();
+ GenericDocument expectedArtist =
+ new GenericDocument.Builder<>("namespace", "id2", "Artist")
+ .setParentTypes(Collections.singletonList("Person"))
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("name", "Foo Artist")
+ .setPropertyString("emailAddress", "[email protected]")
+ .build();
+ assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+ }
+
+ @Test
public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
@@ -1664,4 +585,167 @@
assertThat(documents).hasSize(4);
assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
}
+
+ // TODO(b/336277840): Move this if setParentTypes becomes public
+ @Test
+ public void testQuery_wildcardProjection_polymorphism() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+ AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+ .addProperty(new StringPropertyConfig.Builder("sender")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+ .addParentType("Message")
+ .addProperty(new StringPropertyConfig.Builder("sender")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addParentType("Message")
+ .addProperty(new StringPropertyConfig.Builder("sender")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+ // Index two child documents
+ GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("sender", "Some sender")
+ .setPropertyString("content", "Some note")
+ .build();
+ GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("sender", "Some sender")
+ .setPropertyString("content", "Some note")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, text).build()));
+
+ SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()
+ .addFilterSchemas("Message")
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // We specified the parent document in the filter schemas, but only indexed child documents.
+ // As we also specified a wildcard schema type projection, it should apply to the child docs
+ // The content property must not appear. Also emailNoContent should not appear as we are
+ // filter on the content property
+ GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+ .setParentTypes(Collections.singletonList("Message"))
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("sender", "Some sender")
+ .build();
+ GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")
+ .setParentTypes(Collections.singletonList("Message"))
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("sender", "Some sender")
+ .build();
+ assertThat(documents).containsExactly(expectedText, expectedEmail);
+ }
+
+ // TODO(b/336277840): Move this if setParentTypes becomes public
+ @Test
+ public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+ AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+ .addParentType("Message")
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("carrier")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addParentType("Message")
+ .addProperty(new StringPropertyConfig.Builder("content")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("attachment")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+ // Index two child documents
+ GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("content", "Some note")
+ .setPropertyString("carrier", "Network Inc")
+ .build();
+ GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("content", "Some note")
+ .setPropertyString("attachment", "Network report")
+ .build();
+
+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, text).build()));
+
+ // Both email and text would match for "Network", but only text should match as it is in the
+ // right property
+ SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
+ .addFilterSchemas("Message")
+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // We specified the parent document in the filter schemas, but only indexed child documents.
+ // As we also specified a wildcard schema type projection, it should apply to the child docs
+ // The content property must not appear. Also emailNoContent should not appear as we are
+ // filter on the content property
+ GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+ .setParentTypes(Collections.singletonList("Message"))
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("content", "Some note")
+ .setPropertyString("carrier", "Network Inc")
+ .build();
+ assertThat(documents).containsExactly(expectedText);
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
index 6bcc933..718aaa2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
@@ -26,7 +26,6 @@
import java.util.concurrent.ExecutorService;
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
public class AppSearchSessionLocalInternalTest extends AppSearchSessionInternalTestBase {
@Override
protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index 4165efe..76f32b4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -26,11 +26,8 @@
import com.google.common.util.concurrent.ListenableFuture;
-import org.junit.Test;
-
import java.util.concurrent.ExecutorService;
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AppSearchSessionPlatformInternalTest extends AppSearchSessionInternalTestBase {
@Override
@@ -48,10 +45,4 @@
new PlatformStorage.SearchContext.Builder(context, dbName)
.setWorkerExecutor(executor).build());
}
-
- @Override
- @Test
- public void testSearchSuggestion_propertyFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index 7859463..fc5be8f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -18,9 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
-import android.os.Bundle;
import android.os.Parcel;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+
import org.junit.Test;
import java.util.ArrayList;
@@ -44,7 +45,7 @@
// Serialize the document
Parcel inParcel = Parcel.obtain();
- inParcel.writeBundle(inDoc.getBundle());
+ inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
byte[] data = inParcel.marshall();
inParcel.recycle();
@@ -52,11 +53,13 @@
Parcel outParcel = Parcel.obtain();
outParcel.unmarshall(data, 0, data.length);
outParcel.setDataPosition(0);
- Bundle outBundle = outParcel.readBundle();
+ @SuppressWarnings("deprecation")
+ GenericDocumentParcel documentParcel =
+ outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
outParcel.recycle();
// Compare results
- GenericDocument outDoc = new GenericDocument(outBundle);
+ GenericDocument outDoc = new GenericDocument(documentParcel);
assertThat(inDoc).isEqualTo(outDoc);
assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
@@ -83,7 +86,7 @@
// Serialize the document
Parcel inParcel = Parcel.obtain();
- inParcel.writeBundle(inDoc.getBundle());
+ inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
byte[] data = inParcel.marshall();
inParcel.recycle();
@@ -91,11 +94,13 @@
Parcel outParcel = Parcel.obtain();
outParcel.unmarshall(data, 0, data.length);
outParcel.setDataPosition(0);
- Bundle outBundle = outParcel.readBundle();
+ @SuppressWarnings("deprecation")
+ GenericDocumentParcel documentParcel =
+ outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
outParcel.recycle();
// Compare results
- GenericDocument outDoc = new GenericDocument(outBundle);
+ GenericDocument outDoc = new GenericDocument(documentParcel);
assertThat(inDoc).isEqualTo(outDoc);
assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
@@ -112,47 +117,23 @@
.setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
.setScore(42)
.setPropertyString("propString", "Hello")
- .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
- .setPropertyDocument(
- "propDocument",
- new GenericDocument.Builder<>("namespace", "id2", "schema2")
- .setPropertyString("propString", "Goodbye")
- .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
- .build())
.build();
GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
.setParentTypes(new ArrayList<>(Arrays.asList("Class3", "Class4")))
- .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
- .setPropertyDocument(
- "propDocument",
- new GenericDocument.Builder<>("namespace", "id3", "schema3")
- .setPropertyString("propString", "Bye")
- .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
- .build())
+ .setPropertyString("propString", "Bye")
.build();
// Check that the original GenericDocument is unmodified.
assertThat(oldDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
assertThat(oldDoc.getScore()).isEqualTo(42);
assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
- assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
- assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
- .isEqualTo("Goodbye");
- assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
- .isEqualTo(new byte[][]{{3, 4}});
// Check that the new GenericDocument has modified the original fields correctly.
assertThat(newDoc.getParentTypes()).isEqualTo(Arrays.asList("Class3", "Class4"));
- assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
- assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
- .isEqualTo("Bye");
- assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
- .isEqualTo(new byte[][]{{5, 6}});
+ assertThat(newDoc.getPropertyString("propString")).isEqualTo("Bye");
// Check that the new GenericDocument copies fields that aren't set.
assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
- assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
- "propString"));
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
new file mode 100644
index 0000000..c63e53e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class InternalVisibilityConfigTest {
+
+ @Test
+ public void testVisibilityConfig_setNotDisplayBySystem() {
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+ .setNotDisplayedBySystem(true).build();
+
+ assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+ }
+
+ @Test
+ public void testVisibilityConfig_setVisibilityConfig() {
+ String visibleToPackage1 = "com.example.package";
+ byte[] visibleToPackageCert1 = new byte[32];
+ String visibleToPackage2 = "com.example.package2";
+ byte[] visibleToPackageCert2 = new byte[32];
+
+ SchemaVisibilityConfig innerConfig1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(
+ new PackageIdentifier(visibleToPackage1, visibleToPackageCert1))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+ SchemaVisibilityConfig innerConfig2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(
+ new PackageIdentifier(visibleToPackage2, visibleToPackageCert2))
+ .addRequiredPermissions(ImmutableSet.of(3, 4))
+ .build();
+
+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+ .addVisibleToConfig(innerConfig1)
+ .addVisibleToConfig(innerConfig2)
+ .build();
+
+ assertThat(visibilityConfig.getVisibleToConfigs())
+ .containsExactly(innerConfig1, innerConfig2);
+ }
+
+ @Test
+ public void testToInternalVisibilityConfig() {
+ byte[] packageSha256Cert = new byte[32];
+ packageSha256Cert[0] = 24;
+ packageSha256Cert[8] = 23;
+ packageSha256Cert[16] = 22;
+ packageSha256Cert[24] = 21;
+
+ // Create a SetSchemaRequest for testing
+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("testSchema").build())
+ .setSchemaTypeDisplayedBySystem("testSchema", false)
+ .setSchemaTypeVisibilityForPackage("testSchema", /*visible=*/true,
+ new PackageIdentifier("com.example.test", packageSha256Cert))
+ .setPubliclyVisibleSchema("testSchema",
+ new PackageIdentifier("com.example.test1", packageSha256Cert))
+ .build();
+
+ // Convert the SetSchemaRequest to GenericDocument map
+ List<InternalVisibilityConfig> visibilityConfigs =
+ InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+ // Check if the conversion is correct
+ assertThat(visibilityConfigs).hasSize(1);
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+ assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+ assertThat(visibilityConfig.getVisibilityConfig().getAllowedPackages())
+ .containsExactly(new PackageIdentifier("com.example.test", packageSha256Cert));
+ assertThat(visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage())
+ .isNotNull();
+ assertThat(
+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+ .getPackageName())
+ .isEqualTo("com.example.test1");
+ assertThat(
+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+ .getSha256Certificate())
+ .isEqualTo(packageSha256Cert);
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
new file mode 100644
index 0000000..9f79469
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 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 com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class JoinSpecInternalTest {
+ @Test
+ public void testJoinSpecBuilderCopyConstructor() {
+ JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+ .setMaxJoinedResultCount(10)
+ .setNestedSearch("nestedQuery", new SearchSpec.Builder().build())
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .build();
+ JoinSpec joinSpecCopy = new JoinSpec.Builder(joinSpec).build();
+ assertThat(joinSpecCopy.getMaxJoinedResultCount()).isEqualTo(
+ joinSpec.getMaxJoinedResultCount());
+ assertThat(joinSpecCopy.getChildPropertyExpression()).isEqualTo(
+ joinSpec.getChildPropertyExpression());
+ assertThat(joinSpecCopy.getNestedQuery()).isEqualTo(joinSpec.getNestedQuery());
+ assertThat(joinSpecCopy.getNestedSearchSpec()).isNotNull();
+ assertThat(joinSpecCopy.getAggregationScoringStrategy()).isEqualTo(
+ joinSpec.getAggregationScoringStrategy());
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
new file mode 100644
index 0000000..bec84cb
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 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 com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SearchResultInternalTest {
+ @Test
+ public void testSearchResultBuilderCopyConstructor() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+ SearchResult searchResult = new SearchResult.Builder("package", "database")
+ .setGenericDocument(document)
+ .setRankingSignal(1.23)
+ .addJoinedResult(new SearchResult.Builder("pkg1", "db1").setGenericDocument(
+ document).build())
+ .addJoinedResult(new SearchResult.Builder("pkg2", "db2").setGenericDocument(
+ document).build())
+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath1").build())
+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath2").build())
+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath3").build())
+ .build();
+ SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+ assertThat(searchResultCopy.getGenericDocument()).isEqualTo(
+ searchResult.getGenericDocument());
+ assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+ // Specifically test JoinedResults and MatchInfos with different sizes since briefly had
+ // a bug where we looped through joinedResults using matchInfos.size()
+ assertThat(searchResultCopy.getJoinedResults().size()).isEqualTo(
+ searchResult.getJoinedResults().size());
+ assertThat(searchResultCopy.getJoinedResults().get(0).getPackageName()).isEqualTo("pkg1");
+ assertThat(searchResultCopy.getJoinedResults().get(0).getDatabaseName()).isEqualTo("db1");
+ assertThat(searchResultCopy.getJoinedResults().get(1).getPackageName()).isEqualTo("pkg2");
+ assertThat(searchResultCopy.getJoinedResults().get(1).getDatabaseName()).isEqualTo("db2");
+ assertThat(searchResultCopy.getMatchInfos().size()).isEqualTo(
+ searchResult.getMatchInfos().size());
+ assertThat(searchResultCopy.getMatchInfos().get(0).getPropertyPath()).isEqualTo(
+ "propertyPath1");
+ assertThat(searchResultCopy.getMatchInfos().get(1).getPropertyPath()).isEqualTo(
+ "propertyPath2");
+ assertThat(searchResultCopy.getMatchInfos().get(2).getPropertyPath()).isEqualTo(
+ "propertyPath3");
+ }
+
+ @Test
+ public void testSearchResultBuilderCopyConstructor_informationalRankingSignal() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+ SearchResult searchResult = new SearchResult.Builder("package", "database")
+ .setGenericDocument(document)
+ .setRankingSignal(1.23)
+ .addInformationalRankingSignal(2)
+ .addInformationalRankingSignal(3)
+ .build();
+ SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+ assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+ assertThat(searchResultCopy.getInformationalRankingSignals()).isEqualTo(
+ searchResult.getInformationalRankingSignals());
+ }
+
+ @Test
+ public void testSearchResultBuilder_clearJoinedResults() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+ SearchResult searchResult = new SearchResult.Builder("package", "database")
+ .setGenericDocument(document)
+ .addJoinedResult(new SearchResult.Builder("pkg", "db").setGenericDocument(
+ document).build())
+ .clearJoinedResults()
+ .build();
+ assertThat(searchResult.getJoinedResults()).isEmpty();
+ }
+
+ @Test
+ public void testMatchInfoBuilderCopyConstructor() {
+ SearchResult.MatchRange exactMatchRange = new SearchResult.MatchRange(3, 8);
+ SearchResult.MatchRange submatchRange = new SearchResult.MatchRange(3, 5);
+ SearchResult.MatchRange snippetMatchRange = new SearchResult.MatchRange(1, 10);
+ SearchResult.MatchInfo matchInfo =
+ new SearchResult.MatchInfo.Builder("propertyPath1")
+ .setExactMatchRange(exactMatchRange)
+ .setSubmatchRange(submatchRange)
+ .setSnippetRange(snippetMatchRange).build();
+ SearchResult.MatchInfo matchInfoCopy =
+ new SearchResult.MatchInfo.Builder(matchInfo).build();
+ assertThat(matchInfoCopy.getPropertyPath()).isEqualTo("propertyPath1");
+ assertThat(matchInfoCopy.getExactMatchRange()).isEqualTo(exactMatchRange);
+ assertThat(matchInfoCopy.getSubmatchRange()).isEqualTo(submatchRange);
+ assertThat(matchInfoCopy.getSnippetRange()).isEqualTo(snippetMatchRange);
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
new file mode 100644
index 0000000..e37d1f854
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 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 com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchResultPageInternalTest {
+ @Test
+ public void testSearchResultPage() {
+ GenericDocument document =
+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+ List<SearchResult> results = Arrays.asList(
+ new SearchResult.Builder("package1", "database1").setGenericDocument(
+ document).build(),
+ new SearchResult.Builder("package2", "database2").setGenericDocument(
+ document).build()
+ );
+ SearchResultPage searchResultPage = new SearchResultPage(/*nextPageToken=*/ 123, results);
+ assertThat(searchResultPage.getNextPageToken()).isEqualTo(123);
+ List<SearchResult> searchResults = searchResultPage.getResults();
+ assertThat(searchResults).hasSize(2);
+ assertThat(searchResults.get(0).getPackageName()).isEqualTo("package1");
+ assertThat(searchResults.get(0).getDatabaseName()).isEqualTo("database1");
+ assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(document);
+ assertThat(searchResults.get(1).getPackageName()).isEqualTo("package2");
+ assertThat(searchResults.get(1).getDatabaseName()).isEqualTo("database2");
+ assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(document);
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index ed00e5a..e733fc3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -18,22 +18,17 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
-import android.os.Bundle;
-
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import org.junit.Test;
-import java.util.List;
-import java.util.Map;
+import java.util.Arrays;
/** Tests for private APIs of {@link SearchSpec}. */
public class SearchSpecInternalTest {
@Test
- public void testGetBundle() {
+ public void testSearchSpecBuilder() {
SearchSpec searchSpec = new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("namespace1", "namespace2")
@@ -50,28 +45,135 @@
.setListFilterQueryLanguageEnabled(true)
.build();
- Bundle bundle = searchSpec.getBundle();
- assertThat(bundle.getInt(SearchSpec.TERM_MATCH_TYPE_FIELD))
- .isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
- assertThat(bundle.getStringArrayList(SearchSpec.NAMESPACE_FIELD)).containsExactly(
+ assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+ assertThat(searchSpec.getFilterNamespaces()).containsExactly(
"namespace1", "namespace2");
- assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
+ assertThat(searchSpec.getFilterSchemas()).containsExactly(
"schemaTypes1", "schemaTypes2");
- assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)).containsExactly(
+ assertThat(searchSpec.getFilterPackageNames()).containsExactly(
"package1", "package2");
- assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5);
- assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10);
- assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15);
- assertThat(bundle.getInt(SearchSpec.NUM_PER_PAGE_FIELD)).isEqualTo(42);
- assertThat(bundle.getInt(SearchSpec.ORDER_FIELD)).isEqualTo(SearchSpec.ORDER_ASCENDING);
- assertThat(bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD))
+ assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
+ assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
+ assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
+ assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
+ assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+ assertThat(searchSpec.getRankingStrategy())
.isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
- assertThat(bundle.getStringArrayList(SearchSpec.ENABLED_FEATURES_FIELD)).containsExactly(
+ assertThat(searchSpec.getEnabledFeatures()).containsExactly(
Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
Features.LIST_FILTER_QUERY_LANGUAGE);
}
@Test
+ public void testSearchSpecBuilderCopyConstructor() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .addFilterNamespaces("namespace1", "namespace2")
+ .addFilterSchemas("schemaTypes1", "schemaTypes2")
+ .addFilterPackageNames("package1", "package2")
+ .addFilterProperties("schemaTypes1", Arrays.asList("path1", "path2"))
+ .addProjection("schemaTypes1", Arrays.asList("path1", "path2"))
+ .setPropertyWeights("schemaTypes1", ImmutableMap.of("path1", 1.0, "path2", 2.0))
+ .setSnippetCount(5)
+ .setSnippetCountPerProperty(10)
+ .setMaxSnippetSize(15)
+ .setResultCountPerPage(42)
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .setRankingStrategy("advancedExpression")
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, 10)
+ .setSearchSourceLogTag("searchSourceLogTag")
+ .build();
+
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getTermMatch()).isEqualTo(searchSpec.getTermMatch());
+ assertThat(searchSpecCopy.getFilterNamespaces()).isEqualTo(
+ searchSpec.getFilterNamespaces());
+ assertThat(searchSpecCopy.getFilterSchemas()).isEqualTo(searchSpecCopy.getFilterSchemas());
+ assertThat(searchSpecCopy.getFilterPackageNames()).isEqualTo(
+ searchSpec.getFilterPackageNames());
+ assertThat(searchSpecCopy.getFilterProperties()).isEqualTo(
+ searchSpec.getFilterProperties());
+ assertThat(searchSpecCopy.getProjections()).isEqualTo(searchSpec.getProjections());
+ assertThat(searchSpecCopy.getPropertyWeights()).isEqualTo(searchSpec.getPropertyWeights());
+ assertThat(searchSpecCopy.getSnippetCount()).isEqualTo(searchSpec.getSnippetCount());
+ assertThat(searchSpecCopy.getSnippetCountPerProperty()).isEqualTo(
+ searchSpec.getSnippetCountPerProperty());
+ assertThat(searchSpecCopy.getMaxSnippetSize()).isEqualTo(searchSpec.getMaxSnippetSize());
+ assertThat(searchSpecCopy.getResultCountPerPage()).isEqualTo(
+ searchSpec.getResultCountPerPage());
+ assertThat(searchSpecCopy.getOrder()).isEqualTo(searchSpec.getOrder());
+ assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+ assertThat(searchSpecCopy.getEnabledFeatures()).containsExactlyElementsIn(
+ searchSpec.getEnabledFeatures());
+ assertThat(searchSpecCopy.getResultGroupingTypeFlags()).isEqualTo(
+ searchSpec.getResultGroupingTypeFlags());
+ assertThat(searchSpecCopy.getResultGroupingLimit()).isEqualTo(
+ searchSpec.getResultGroupingLimit());
+ assertThat(searchSpecCopy.getJoinSpec()).isEqualTo(searchSpec.getJoinSpec());
+ assertThat(searchSpecCopy.getAdvancedRankingExpression()).isEqualTo(
+ searchSpec.getAdvancedRankingExpression());
+ assertThat(searchSpecCopy.getSearchSourceLogTag()).isEqualTo(
+ searchSpec.getSearchSourceLogTag());
+ }
+
+ @Test
+ public void testSearchSpecBuilderCopyConstructor_embeddingSearch() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(embedding1, embedding2)
+ .build();
+
+ // Check that copy constructor works.
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getEnabledFeatures()).containsExactlyElementsIn(
+ searchSpec.getEnabledFeatures());
+ assertThat(searchSpecCopy.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+ searchSpec.getDefaultEmbeddingSearchMetricType());
+ assertThat(searchSpecCopy.getSearchEmbeddings()).containsExactlyElementsIn(
+ searchSpec.getSearchEmbeddings());
+ }
+
+ @Test
+ public void testSearchSpecBuilderCopyConstructor_informationalRankingExpressions() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setRankingStrategy("advancedExpression")
+ .addInformationalRankingExpressions("this.relevanceScore()")
+ .build();
+
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+ assertThat(searchSpecCopy.getAdvancedRankingExpression()).isEqualTo(
+ searchSpec.getAdvancedRankingExpression());
+ assertThat(searchSpecCopy.getInformationalRankingExpressions()).isEqualTo(
+ searchSpec.getInformationalRankingExpressions());
+ }
+
+ // TODO(b/309826655): Flag guard this test.
+ @Test
+ public void testGetBundle_hasProperty() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .build();
+
+ assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+ Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+ Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION);
+ }
+
+ @Test
public void testBuildMultipleSearchSpecs() {
SearchSpec.Builder builder = new SearchSpec.Builder();
SearchSpec searchSpec1 = builder.build();
@@ -90,37 +192,46 @@
Features.VERBATIM_SEARCH, Features.LIST_FILTER_QUERY_LANGUAGE);
}
- // TODO(b/296088047): move to CTS once the APIs it uses are public
@Test
- public void testGetPropertyFiltersTypePropertyMasks() {
+ public void testGetEnabledFeatures_embeddingSearch() {
SearchSpec searchSpec = new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
- .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
- .addFilterProperties("TypeB", ImmutableList.of("field7"))
- .addFilterProperties("TypeC", ImmutableList.of())
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .setEmbeddingSearchEnabled(true)
.build();
+ assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+ Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+ Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+ FeatureConstants.EMBEDDING_SEARCH);
- Map<String, List<String>> typePropertyPathMap = searchSpec.getFilterProperties();
- assertThat(typePropertyPathMap.keySet())
- .containsExactly("TypeA", "TypeB", "TypeC");
- assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
- assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
- assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+ // Check that copy constructor works.
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getEnabledFeatures()).containsExactly(
+ Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+ Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+ FeatureConstants.EMBEDDING_SEARCH);
}
- // TODO(b/296088047): move to CTS once the APIs it uses are public
@Test
- public void testBuilder_throwsException_whenTypePropertyFilterNotInSchemaFilter() {
- SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
- .addFilterSchemas("Schema1", "Schema2")
- .addFilterPropertyPaths("Schema3", ImmutableList.of(
- new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
+ public void testGetEnabledFeatures_tokenize() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterTokenizeFunctionEnabled(true)
+ .build();
+ assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+ Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+ Features.LIST_FILTER_QUERY_LANGUAGE,
+ FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
- IllegalStateException exception =
- assertThrows(IllegalStateException.class, searchSpecBuilder::build);
- assertThat(exception.getMessage())
- .isEqualTo("The schema: Schema3 exists in the property filter but doesn't"
- + " exist in the schema filter.");
+ // Check that copy constructor works.
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getEnabledFeatures()).containsExactly(
+ Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+ Features.LIST_FILTER_QUERY_LANGUAGE,
+ FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
deleted file mode 100644
index bc68f37..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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 com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Test;
-
-// TODO(b/228240987) delete this test when we support property restrict for multiple terms
-public class SearchSuggestionSpecInternalTest {
-
- @Test
- public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
- SearchSuggestionSpec searchSuggestionSpec =
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
- .setRankingStrategy(SearchSuggestionSpec
- .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
- .addFilterSchemas("Person", "Email")
- .addFilterSchemas(ImmutableList.of("Foo"))
- .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
- .addFilterPropertyPaths("Foo",
- ImmutableList.of(new PropertyPath("Bar")))
- .build();
-
- assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
- assertThat(searchSuggestionSpec.getFilterSchemas())
- .containsExactly("Person", "Email", "Foo");
- assertThat(searchSuggestionSpec.getFilterProperties())
- .containsExactly("Email", ImmutableList.of("Subject", "body"),
- "Foo", ImmutableList.of("Bar"));
- }
-
- @Test
- public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
- IllegalStateException e = assertThrows(IllegalStateException.class,
- () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
- .addFilterSchemas("Person")
- .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
- .build());
- assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
- + "property filter but doesn't exist in the schema filter.");
- }
-
- @Test
- public void testRebuild_withPropertyFilter() throws Exception {
- SearchSuggestionSpec.Builder builder =
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
- .addFilterSchemas("Person", "Email")
- .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
-
- SearchSuggestionSpec original = builder.build();
-
- builder.addFilterSchemas("Message", "Foo")
- .addFilterProperties("Foo", ImmutableList.of("Bar"));
- SearchSuggestionSpec rebuild = builder.build();
-
- assertThat(original.getMaximumResultCount()).isEqualTo(123);
- assertThat(original.getFilterSchemas())
- .containsExactly("Person", "Email");
- assertThat(original.getFilterProperties())
- .containsExactly("Email", ImmutableList.of("Subject", "body"));
-
- assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
- assertThat(rebuild.getFilterSchemas())
- .containsExactly("Person", "Email", "Message", "Foo");
- assertThat(rebuild.getFilterProperties())
- .containsExactly("Email", ImmutableList.of("Subject", "body"),
- "Foo", ImmutableList.of("Bar"));
- }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
index 37e1255..145679a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
@@ -18,8 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
@@ -55,7 +53,7 @@
assertThat(original.getMigratedTypes()).containsExactly("migrated1");
assertThat(original.getMigrationFailures()).containsExactly(failure1);
- SetSchemaResponse rebuild = original.toBuilder()
+ SetSchemaResponse rebuild = new SetSchemaResponse.Builder(original)
.addDeletedType("delete2")
.addIncompatibleType("incompatible2")
.addMigratedType("migrated2")
@@ -82,7 +80,6 @@
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .setDeletionPropagation(true)
.build())
.build();
@@ -95,20 +92,5 @@
.isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
assertThat(((StringPropertyConfig) properties.get(0)).getJoinableValueType())
.isEqualTo(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
- assertThat(((StringPropertyConfig) properties.get(0)).getDeletionPropagation())
- .isEqualTo(true);
- }
-
- // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
- @Test
- public void testStringPropertyConfig_setJoinableProperty_deletePropagationError() {
- final StringPropertyConfig.Builder builder =
- new StringPropertyConfig.Builder("qualifiedId")
- .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
- .setDeletionPropagation(true);
- IllegalStateException e =
- assertThrows(IllegalStateException.class, () -> builder.build());
- assertThat(e).hasMessageThat().contains(
- "Cannot set deletion propagation without setting a joinable value type");
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 4118c45..0e4952c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -25,13 +25,23 @@
import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.PropertyPath;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import androidx.appsearch.testutil.AppSearchEmail;
+import org.junit.Rule;
import org.junit.Test;
import java.util.Collections;
+import java.util.List;
public class AppSearchSchemaCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testInvalidEnums() {
StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
@@ -165,6 +175,243 @@
}
@Test
+ public void testParentTypes() {
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("EmailMessage")
+ .addParentType("Email")
+ .addParentType("Message")
+ .build();
+ assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+ }
+
+ @Test
+ public void testDuplicateParentTypes() {
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("EmailMessage")
+ .addParentType("Email")
+ .addParentType("Message")
+ .addParentType("Email")
+ .build();
+ assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+ }
+
+ @Test
+ public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {
+ AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+ new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+ .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")
+ .build();
+ assertThat(documentPropertyConfig.getIndexableNestedProperties())
+ .containsExactly("prop1", "prop2", "prop1.prop2");
+ }
+
+ @Test
+ public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {
+ AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+ new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+ .addIndexableNestedPropertyPaths(
+ new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
+ .build();
+ assertThat(documentPropertyConfig.getIndexableNestedProperties())
+ .containsExactly("prop1", "prop1.prop2");
+ }
+
+ @Test
+ public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {
+ AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+ new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+ .addIndexableNestedPropertyPaths(
+ new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
+ .addIndexableNestedProperties("prop1")
+ .build();
+ assertThat(documentPropertyConfig.getIndexableNestedProperties())
+ .containsExactly("prop1", "prop1.prop2");
+ }
+
+ @Test
+ public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {
+ AppSearchSchema.DocumentPropertyConfig.Builder builder =
+ new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+ .addIndexableNestedProperties("prop1");
+ AppSearchSchema.DocumentPropertyConfig config1 = builder.build();
+ assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+
+ builder.addIndexableNestedProperties("prop2");
+ AppSearchSchema.DocumentPropertyConfig config2 = builder.build();
+ assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
+ assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+
+ builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));
+ AppSearchSchema.DocumentPropertyConfig config3 = builder.build();
+ assertThat(config3.getIndexableNestedProperties())
+ .containsExactly("prop1", "prop2", "prop3");
+ assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
+ assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+ }
+
+ @Test
+ public void testPropertyConfig() {
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("Test")
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("string")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig
+ .INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig
+ .TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("long")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.LongPropertyConfig
+ .INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.LongPropertyConfig
+ .INDEXING_TYPE_RANGE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DoublePropertyConfig.Builder("double")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document1", AppSearchEmail.SCHEMA_TYPE)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document2", AppSearchEmail.SCHEMA_TYPE)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(false)
+ .addIndexableNestedProperties("path1", "path2", "path3")
+ .build())
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(
+ AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build())
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .setJoinableValueType(
+ AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build())
+ .build();
+
+ assertThat(schema.getSchemaType()).isEqualTo("Test");
+ List<PropertyConfig> properties = schema.getProperties();
+ assertThat(properties).hasSize(10);
+
+ assertThat(properties.get(0).getName()).isEqualTo("string");
+ assertThat(properties.get(0).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+ assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
+ .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+ assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(
+ 0)).getTokenizerType())
+ .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+ assertThat(properties.get(1).getName()).isEqualTo("long");
+ assertThat(properties.get(1).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
+ .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);
+
+ assertThat(properties.get(2).getName()).isEqualTo("indexableLong");
+ assertThat(properties.get(2).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())
+ .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
+
+ assertThat(properties.get(3).getName()).isEqualTo("double");
+ assertThat(properties.get(3).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+ assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
+
+ assertThat(properties.get(4).getName()).isEqualTo("boolean");
+ assertThat(properties.get(4).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+ assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
+
+ assertThat(properties.get(5).getName()).isEqualTo("bytes");
+ assertThat(properties.get(5).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
+
+ assertThat(properties.get(6).getName()).isEqualTo("document1");
+ assertThat(properties.get(6).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+ assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())
+ .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+ assertThat(
+ ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))
+ .shouldIndexNestedProperties())
+ .isEqualTo(true);
+
+ assertThat(properties.get(7).getName()).isEqualTo("document2");
+ assertThat(properties.get(7).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+ assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())
+ .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+ assertThat(
+ ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
+ .shouldIndexNestedProperties())
+ .isEqualTo(false);
+ assertThat(
+ ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
+ .getIndexableNestedProperties())
+ .containsExactly("path1", "path2", "path3");
+
+ assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");
+ assertThat(properties.get(8).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(
+ ((AppSearchSchema.StringPropertyConfig) properties.get(8))
+ .getJoinableValueType())
+ .isEqualTo(
+ AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+
+ assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");
+ assertThat(properties.get(9).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+ assertThat(
+ ((AppSearchSchema.StringPropertyConfig) properties.get(9))
+ .getJoinableValueType())
+ .isEqualTo(
+ AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+ }
+
+ @Test
public void testInvalidStringPropertyConfigsTokenizerNone() {
// Everything should work fine with the defaults.
final StringPropertyConfig.Builder builder =
@@ -195,6 +442,57 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testEquals_failure_differentDescription() {
+ AppSearchSchema.Builder schemaBuilder =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ AppSearchSchema schema1 = schemaBuilder.build();
+ AppSearchSchema schema2 =
+ schemaBuilder.setDescription("Mail, but like with an 'e'").build();
+ assertThat(schema1).isNotEqualTo(schema2);
+ assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testEquals_failure_differentPropertyDescription() {
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("A summary of the contents of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ AppSearchSchema schema2 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("The beginning of a message.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+ assertThat(schema1).isNotEqualTo(schema2);
+ assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+ }
+
+ @Test
public void testInvalidStringPropertyConfigsTokenizerNonNone() {
// Setting indexing type to be NONE with tokenizer type PLAIN or VERBATIM or RFC822 should
// fail. Regardless of whether NONE is set explicitly or just kept as default.
@@ -250,162 +548,253 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
public void testAppSearchSchema_toString() {
- AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
- .addProperty(new StringPropertyConfig.Builder("string1")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
- .build())
- .addProperty(new StringPropertyConfig.Builder("string2")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("string3")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
- .addProperty(new StringPropertyConfig.Builder("string4")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
- .build())
- .addProperty(new StringPropertyConfig.Builder("string5")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
- .build())
- .addProperty(new StringPropertyConfig.Builder("qualifiedId1")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .build())
- .addProperty(new StringPropertyConfig.Builder("qualifiedId2")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
- .build())
- .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
- .build())
- .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
- .build())
- .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
- .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
- .build())
- .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
- .build())
- .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .build())
- .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
- "document", AppSearchEmail.SCHEMA_TYPE)
- .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
- .setShouldIndexNestedProperties(true)
- .build())
- .build();
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("testSchema")
+ .setDescription("a test schema")
+ .addProperty(
+ new StringPropertyConfig.Builder("string1")
+ .setDescription("first string")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("string2")
+ .setDescription("second string")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("string3")
+ .setDescription("third string")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("string4")
+ .setDescription("fourth string")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("string5")
+ .setDescription("fifth string")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("qualifiedId1")
+ .setDescription("first qualifiedId")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setJoinableValueType(
+ StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("qualifiedId2")
+ .setDescription("second qualifiedId")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(
+ StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("long")
+ .setDescription("a long")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+ .setDescription("an indexed long")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DoublePropertyConfig.Builder("double")
+ .setDescription("a double")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+ .setDescription("a boolean")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+ .setDescription("some bytes")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document", AppSearchEmail.SCHEMA_TYPE)
+ .setDescription("a document")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build();
String schemaString = schema.toString();
- String expectedString = "{\n"
- + " schemaType: \"testSchema\",\n"
- + " properties: [\n"
- + " {\n"
- + " name: \"boolean\",\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_BOOLEAN,\n"
- + " },\n"
- + " {\n"
- + " name: \"bytes\",\n"
- + " cardinality: CARDINALITY_OPTIONAL,\n"
- + " dataType: DATA_TYPE_BYTES,\n"
- + " },\n"
- + " {\n"
- + " name: \"document\",\n"
- + " shouldIndexNestedProperties: true,\n"
- + " schemaType: \"builtin:Email\",\n"
- + " cardinality: CARDINALITY_REPEATED,\n"
- + " dataType: DATA_TYPE_DOCUMENT,\n"
- + " },\n"
- + " {\n"
- + " name: \"double\",\n"
- + " cardinality: CARDINALITY_REPEATED,\n"
- + " dataType: DATA_TYPE_DOUBLE,\n"
- + " },\n"
- + " {\n"
- + " name: \"indexableLong\",\n"
- + " indexingType: INDEXING_TYPE_RANGE,\n"
- + " cardinality: CARDINALITY_OPTIONAL,\n"
- + " dataType: DATA_TYPE_LONG,\n"
- + " },\n"
- + " {\n"
- + " name: \"long\",\n"
- + " indexingType: INDEXING_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_OPTIONAL,\n"
- + " dataType: DATA_TYPE_LONG,\n"
- + " },\n"
- + " {\n"
- + " name: \"qualifiedId1\",\n"
- + " indexingType: INDEXING_TYPE_NONE,\n"
- + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"qualifiedId2\",\n"
- + " indexingType: INDEXING_TYPE_NONE,\n"
- + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
- + " cardinality: CARDINALITY_OPTIONAL,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"string1\",\n"
- + " indexingType: INDEXING_TYPE_NONE,\n"
- + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"string2\",\n"
- + " indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
- + " tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"string3\",\n"
- + " indexingType: INDEXING_TYPE_PREFIXES,\n"
- + " tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"string4\",\n"
- + " indexingType: INDEXING_TYPE_PREFIXES,\n"
- + " tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " },\n"
- + " {\n"
- + " name: \"string5\",\n"
- + " indexingType: INDEXING_TYPE_PREFIXES,\n"
- + " tokenizerType: TOKENIZER_TYPE_RFC822,\n"
- + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
- + " cardinality: CARDINALITY_REQUIRED,\n"
- + " dataType: DATA_TYPE_STRING,\n"
- + " }\n"
- + " ]\n"
- + "}";
+ String expectedString =
+ "{\n"
+ + " schemaType: \"testSchema\",\n"
+ + " description: \"a test schema\",\n"
+ + " properties: [\n"
+ + " {\n"
+ + " name: \"boolean\",\n"
+ + " description: \"a boolean\",\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_BOOLEAN,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"bytes\",\n"
+ + " description: \"some bytes\",\n"
+ + " cardinality: CARDINALITY_OPTIONAL,\n"
+ + " dataType: DATA_TYPE_BYTES,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"document\",\n"
+ + " description: \"a document\",\n"
+ + " shouldIndexNestedProperties: true,\n"
+ + " schemaType: \"builtin:Email\",\n"
+ + " cardinality: CARDINALITY_REPEATED,\n"
+ + " dataType: DATA_TYPE_DOCUMENT,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"double\",\n"
+ + " description: \"a double\",\n"
+ + " cardinality: CARDINALITY_REPEATED,\n"
+ + " dataType: DATA_TYPE_DOUBLE,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"indexableLong\",\n"
+ + " description: \"an indexed long\",\n"
+ + " indexingType: INDEXING_TYPE_RANGE,\n"
+ + " cardinality: CARDINALITY_OPTIONAL,\n"
+ + " dataType: DATA_TYPE_LONG,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"long\",\n"
+ + " description: \"a long\",\n"
+ + " indexingType: INDEXING_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_OPTIONAL,\n"
+ + " dataType: DATA_TYPE_LONG,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"qualifiedId1\",\n"
+ + " description: \"first qualifiedId\",\n"
+ + " indexingType: INDEXING_TYPE_NONE,\n"
+ + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"qualifiedId2\",\n"
+ + " description: \"second qualifiedId\",\n"
+ + " indexingType: INDEXING_TYPE_NONE,\n"
+ + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+ + " cardinality: CARDINALITY_OPTIONAL,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"string1\",\n"
+ + " description: \"first string\",\n"
+ + " indexingType: INDEXING_TYPE_NONE,\n"
+ + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"string2\",\n"
+ + " description: \"second string\",\n"
+ + " indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
+ + " tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"string3\",\n"
+ + " description: \"third string\",\n"
+ + " indexingType: INDEXING_TYPE_PREFIXES,\n"
+ + " tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"string4\",\n"
+ + " description: \"fourth string\",\n"
+ + " indexingType: INDEXING_TYPE_PREFIXES,\n"
+ + " tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " },\n"
+ + " {\n"
+ + " name: \"string5\",\n"
+ + " description: \"fifth string\",\n"
+ + " indexingType: INDEXING_TYPE_PREFIXES,\n"
+ + " tokenizerType: TOKENIZER_TYPE_RFC822,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " }\n"
+ + " ]\n"
+ + "}";
+
+ String[] lines = expectedString.split("\n");
+ for (String line : lines) {
+ assertThat(schemaString).contains(line);
+ }
+ }
+
+ @Test
+ public void testAppSearchSchema_toStringNoDescriptionSet() {
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("testSchema")
+ .addProperty(
+ new StringPropertyConfig.Builder("string1")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .build())
+ .build();
+
+ String schemaString = schema.toString();
+
+ String expectedString =
+ "{\n"
+ + " schemaType: \"testSchema\",\n"
+ + " description: \"\",\n"
+ + " properties: [\n"
+ + " {\n"
+ + " name: \"string1\",\n"
+ + " description: \"\",\n"
+ + " indexingType: INDEXING_TYPE_NONE,\n"
+ + " tokenizerType: TOKENIZER_TYPE_NONE,\n"
+ + " joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+ + " cardinality: CARDINALITY_REQUIRED,\n"
+ + " dataType: DATA_TYPE_STRING,\n"
+ + " }\n"
+ + " ]\n"
+ + "}";
String[] lines = expectedString.split("\n");
for (String line : lines) {
@@ -466,4 +855,120 @@
"DocumentIndexingConfig#shouldIndexNestedProperties is required to be false"
+ " when one or more indexableNestedProperties are provided.");
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingPropertyConfig() {
+ AppSearchSchema schema =
+ new AppSearchSchema.Builder("Test")
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("string")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig
+ .INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig
+ .TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.LongPropertyConfig
+ .INDEXING_TYPE_RANGE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "document1", AppSearchEmail.SCHEMA_TYPE)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .addProperty(
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig
+ .INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder(
+ "indexableEmbedding")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig
+ .INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+
+ assertThat(schema.getSchemaType()).isEqualTo("Test");
+ List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
+ assertThat(properties).hasSize(5);
+
+ assertThat(properties.get(0).getName()).isEqualTo("string");
+ assertThat(properties.get(0).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+ assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
+ .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+ assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
+ .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+ assertThat(properties.get(1).getName()).isEqualTo("indexableLong");
+ assertThat(properties.get(1).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
+ .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
+
+ assertThat(properties.get(2).getName()).isEqualTo("document1");
+ assertThat(properties.get(2).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+ assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(2)).getSchemaType())
+ .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+ assertThat(
+ ((AppSearchSchema.DocumentPropertyConfig) properties.get(2))
+ .shouldIndexNestedProperties())
+ .isEqualTo(true);
+
+ assertThat(properties.get(3).getName()).isEqualTo("embedding");
+ assertThat(properties.get(3).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(3)).getIndexingType())
+ .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+
+ assertThat(properties.get(4).getName()).isEqualTo("indexableEmbedding");
+ assertThat(properties.get(4).getCardinality())
+ .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(4)).getIndexingType())
+ .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingPropertyConfig_defaultValues() {
+ AppSearchSchema.EmbeddingPropertyConfig builder =
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("test").build();
+ assertThat(builder.getIndexingType()).isEqualTo(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+ assertThat(builder.getCardinality()).isEqualTo(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingPropertyConfig_setIndexingType() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+ .setIndexingType(5).build());
+ assertThrows(IllegalArgumentException.class, () ->
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+ .setIndexingType(2).build());
+ assertThrows(IllegalArgumentException.class, () ->
+ new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+ .setIndexingType(-1).build());
+ }
}
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 59c17f5..895a58a 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
@@ -154,9 +154,11 @@
}
@Test
- public void testSchemaMigration_A_B_C_D() throws Exception {
+ public void test_ForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards compatible schema and update the version
- AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
@@ -166,7 +168,7 @@
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.setVersion(2) // upgrade version
@@ -174,9 +176,11 @@
}
@Test
- public void testSchemaMigration_A_B_NC_D() throws Exception {
+ public void testForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards compatible schema but don't update the version
- AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
@@ -186,20 +190,22 @@
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build()).get();
}
@Test
- public void testSchemaMigration_A_NB_C_D() throws Exception {
+ public void testForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema and update the version
- AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.setVersion(2) // upgrade version
@@ -207,13 +213,15 @@
}
@Test
- public void testSchemaMigration_A_NB_C_ND() throws Exception {
+ public void testForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema and update the version
- AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setForceOverride(true)
.setVersion(2) // upgrade version
@@ -221,35 +229,42 @@
}
@Test
- public void testSchemaMigration_A_NB_NC_D() throws Exception {
+ public void testForceOverride_BackwardsIncompatible_NoTrigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema but don't update the version
- AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleNoTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleNoTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build()).get();
}
@Test
- public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+ public void testForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema but don't update the version
- AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleNoMigrateIncompatibleTypeSchema =
+ new AppSearchSchema.Builder("testSchema")
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(
+ backwardsIncompatibleNoMigrateIncompatibleTypeSchema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setForceOverride(true)
.build()).get();
}
@Test
- public void testSchemaMigration_NA_B_C_D() throws Exception {
+ public void testNoForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards compatible schema and update the version
- AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
@@ -259,16 +274,18 @@
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
.build()).get();
}
@Test
- public void testSchemaMigration_NA_B_NC_D() throws Exception {
+ public void testNoForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards compatible schema but don't update the version
- AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
@@ -278,34 +295,38 @@
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build()).get();
}
@Test
- public void testSchemaMigration_NA_NB_C_D() throws Exception {
+ public void testNoForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema and update the version
- AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.build();
mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
.build()).get();
}
@Test
- public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+ public void testNoForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema and update the version
- AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+ .Builder("testSchema")
.build();
ExecutionException exception = assertThrows(ExecutionException.class,
() -> mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.setVersion(2) // upgrade version
.build()).get());
@@ -313,14 +334,17 @@
}
@Test
- public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+ public void testNoForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+ throws Exception {
// create a backwards incompatible schema but don't update the version
- AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+ AppSearchSchema backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema =
+ new AppSearchSchema.Builder("testSchema")
.build();
ExecutionException exception = assertThrows(ExecutionException.class,
() -> mDb.setSchemaAsync(
- new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+ new SetSchemaRequest.Builder().addSchemas(
+ backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) //ND
.build()).get());
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
@@ -704,7 +728,7 @@
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
- Migrator migrator_sourceToNowhere = new Migrator() {
+ Migrator migratorSourceToNowhere = new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
@@ -732,7 +756,7 @@
ExecutionException exception = assertThrows(ExecutionException.class,
() -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("sourceSchema", migrator_sourceToNowhere)
+ .setMigrator("sourceSchema", migratorSourceToNowhere)
.setVersion(2).build()) // upgrade version
.get());
assertThat(exception).hasMessageThat().contains(
@@ -744,7 +768,7 @@
exception = assertThrows(ExecutionException.class,
() -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("sourceSchema", migrator_sourceToNowhere)
+ .setMigrator("sourceSchema", migratorSourceToNowhere)
.setForceOverride(true)
.setVersion(2).build()) // upgrade version
.get());
@@ -761,7 +785,7 @@
mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(destinationSchema).setForceOverride(true).build()).get();
- Migrator migrator_nowhereToDestination = new Migrator() {
+ Migrator migratorNowhereToDestination = new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
@@ -789,7 +813,7 @@
SetSchemaResponse setSchemaResponse =
mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+ .setMigrator("nonExistSchema", migratorNowhereToDestination)
.setVersion(2) // upgrade version
.build()).get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -798,7 +822,7 @@
setSchemaResponse =
mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+ .setMigrator("nonExistSchema", migratorNowhereToDestination)
.setVersion(2) // upgrade version
.setForceOverride(true).build()).get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -809,7 +833,7 @@
// set empty schema
mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.setForceOverride(true).build()).get();
- Migrator migrator_nowhereToNowhere = new Migrator() {
+ Migrator migratorNowhereToNowhere = new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
@@ -837,7 +861,7 @@
SetSchemaResponse setSchemaResponse =
mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+ .setMigrator("nonExistSchema", migratorNowhereToNowhere)
.setVersion(2) // upgrade version
.build()).get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -846,7 +870,7 @@
setSchemaResponse =
mDb.setSchemaAsync(new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("emptySchema").build())
- .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+ .setMigrator("nonExistSchema", migratorNowhereToNowhere)
.setVersion(2) // upgrade version
.setForceOverride(true).build()).get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -1208,7 +1232,7 @@
@Override
public GenericDocument onUpgrade(int currentVersion, int finalVersion,
@NonNull GenericDocument document) {
- GenericDocument.Builder docBuilder =
+ GenericDocument.Builder<?> docBuilder =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
if (currentVersion == 2) {
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 672c0f3..97698a7 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
@@ -41,6 +41,7 @@
import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetByDocumentIdRequest;
@@ -51,6 +52,7 @@
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
@@ -60,7 +62,13 @@
import androidx.appsearch.app.StorageInfo;
import androidx.appsearch.cts.app.customer.EmailDocument;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
import androidx.appsearch.util.DocumentIdUtil;
import androidx.collection.ArrayMap;
import androidx.test.core.app.ApplicationProvider;
@@ -73,6 +81,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
@@ -89,6 +98,14 @@
static final String DB_NAME_1 = "";
static final String DB_NAME_2 = "testDb2";
+ // Since we cannot call non-public API in the cts test, make a copy of these 2 action types, so
+ // we can create taken actions in GenericDocument form.
+ private static final int ACTION_TYPE_SEARCH = 1;
+ private static final int ACTION_TYPE_CLICK = 2;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private final Context mContext = ApplicationProvider.getApplicationContext();
private AppSearchSession mDb1;
@@ -170,6 +187,178 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testSetSchema_schemaDescription_notSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SCHEMA_SET_DESCRIPTION));
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+ .setDescription("Unsupported description")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema)
+ .build();
+
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.setSchemaAsync(request).get());
+ assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testSetSchema_propertyDescription_notSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SCHEMA_SET_DESCRIPTION));
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setDescription("Unsupported description")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema)
+ .build();
+
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.setSchemaAsync(request).get());
+ assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testSetSchema_updateSchemaDescription() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message.")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("A summary of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setDescription("All of the content of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+ .get();
+
+ Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+ assertThat(actualSchemaTypes).containsExactly(schema1);
+
+ // Change the type description.
+ AppSearchSchema schema2 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("Like mail but with an 'a'.")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("A summary of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setDescription("All of the content of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+ .get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
+ public void testSetSchema_updatePropertyDescription() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message.")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("A summary of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setDescription("All of the content of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+ .get();
+
+ Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+ assertThat(actualSchemaTypes).containsExactly(schema1);
+
+ // Change the type description.
+ AppSearchSchema schema2 =
+ new AppSearchSchema.Builder("Email")
+ .setDescription("A type of electronic message.")
+ .addProperty(
+ new StringPropertyConfig.Builder("subject")
+ .setDescription("The most important part of the email.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setDescription("All the other stuff.")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+ .get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+ }
+
+ @Test
public void testSetSchema_updateVersion() throws Exception {
AppSearchSchema schema = new AppSearchSchema.Builder("Email")
.addProperty(new StringPropertyConfig.Builder("subject")
@@ -420,6 +609,46 @@
}
@Test
+ public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+ // Create schema with invalid cycle: Person -> Organization -> Person... where all
+ // DocumentPropertyConfigs have setShouldIndexNestedProperties(true).
+ AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build();
+ AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
+ .addProperty(new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build();
+
+ SetSchemaRequest setSchemaRequest =
+ new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
+ ExecutionException executionException =
+ assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(setSchemaRequest).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) executionException.getCause();
+ assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+ assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
+ }
+
+ @Test
public void testSetSchemaWithValidCycle_circularReferencesNotSupported() {
assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
@@ -467,8 +696,6 @@
}
// @exportToFramework:endStrip()
-// @exportToFramework:startStrip()
-
/** Test indexing maximum properties into a schema. */
@Test
public void testSetSchema_maxProperties() throws Exception {
@@ -485,11 +712,89 @@
Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
assertThat(actual1).containsExactly(maxSchema);
- // TODO(b/300135897): Expand test to assert adding more than allowed properties is
- // fixed once fixed.
+ schemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ ExecutionException exception = assertThrows(ExecutionException.class, () ->
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .addSchemas(schemaBuilder.build()).setForceOverride(true).build()).get());
+ Throwable cause = exception.getCause();
+ assertThat(cause).isInstanceOf(AppSearchException.class);
+ assertThat(cause.getMessage()).isEqualTo("Too many properties to be indexed, max "
+ + "number of properties allowed: " + maxProperties);
}
@Test
+ public void testSetSchema_maxProperties_nestedSchemas() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+ int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
+ AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+ for (int i = 0; i < maxProperties / 3 + 1; i++) {
+ personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ }
+ personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("worksFor",
+ "Organization")
+ .setShouldIndexNestedProperties(false)
+ .build());
+ personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+ .setShouldIndexNestedProperties(true)
+ .build());
+
+ AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization");
+ for (int i = 0; i < maxProperties / 3; i++) {
+ orgSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ }
+ orgSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+ .setShouldIndexNestedProperties(true)
+ .build());
+
+ AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address");
+ for (int i = 0; i < maxProperties / 3; i++) {
+ addressSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ }
+
+ AppSearchSchema personSchema = personSchemaBuilder.build();
+ AppSearchSchema orgSchema = orgSchemaBuilder.build();
+ AppSearchSchema addressSchema = addressSchemaBuilder.build();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(personSchema, orgSchema, addressSchema)
+ .build()).get();
+ Set<AppSearchSchema> schemas = mDb1.getSchemaAsync().get().getSchemas();
+ assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema);
+
+ // Add one more property to bring the number of sections over the max limit
+ personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build());
+ ExecutionException exception = assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(personSchemaBuilder.build(), orgSchema, addressSchema)
+ .setForceOverride(true)
+ .build()
+ ).get());
+ Throwable cause = exception.getCause();
+ assertThat(cause).isInstanceOf(AppSearchException.class);
+ assertThat(cause.getMessage()).contains("Too many properties to be indexed");
+ }
+
+// @exportToFramework:startStrip()
+
+ @Test
public void testGetSchema() throws Exception {
AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
.addProperty(new StringPropertyConfig.Builder("subject")
@@ -672,6 +977,58 @@
}
@Test
+ public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+
+ AppSearchSchema noteSchema = new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("subject").build()).build();
+ SetSchemaRequest.Builder requestBuilder = new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA, noteSchema)
+ .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false)
+ .setSchemaTypeVisibilityForPackage(
+ noteSchema.getSchemaType(),
+ true,
+ new PackageIdentifier("com.some.package1", new byte[32]))
+ .addRequiredPermissionsForSchemaTypeVisibility(
+ noteSchema.getSchemaType(),
+ Collections.singleton(SetSchemaRequest.READ_SMS));
+ if (mDb1.getFeatures().isFeatureSupported(
+ Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+ requestBuilder.setPubliclyVisibleSchema(
+ noteSchema.getSchemaType(),
+ new PackageIdentifier("com.some.package2", new byte[32]));
+ }
+ SetSchemaRequest request = requestBuilder.build();
+ mDb1.setSchemaAsync(request).get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+ assertThat(actual).hasSize(2);
+ assertThat(actual).isEqualTo(request.getSchemas());
+
+ // Check visibility settings. Schemas without settings shouldn't appear in the result at
+ // all, even with empty maps as values.
+ assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+ .containsExactly(noteSchema.getSchemaType());
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
+ .containsExactly(
+ noteSchema.getSchemaType(),
+ ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32])));
+ assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
+ .containsExactly(
+ noteSchema.getSchemaType(),
+ ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
+ if (mDb1.getFeatures().isFeatureSupported(
+ Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+ assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+ .containsExactly(
+ noteSchema.getSchemaType(),
+ new PackageIdentifier("com.some.package2", new byte[32]));
+ }
+ }
+
+ @Test
public void testGetSchema_visibilitySetting_notSupported() throws Exception {
assumeFalse(mDb1.getFeatures().isFeatureSupported(
Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
@@ -740,6 +1097,101 @@
}
@Test
+ public void testSetSchema_publiclyVisible() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+ PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]);
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+ .setPubliclyVisibleSchema("builtin:Email", pkg).build();
+
+ mDb1.setSchemaAsync(request).get();
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+ assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+ .isEqualTo(ImmutableMap.of("builtin:Email", pkg));
+
+ AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+ .setSubject("testPut example").build();
+
+ // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we
+ // can make sure they their own documents under the Public ACL.
+ AppSearchBatchResult<String, Void> putResult =
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+ assertThat(putResult.getSuccesses()).containsExactly("id1", null);
+ assertThat(putResult.getFailures()).isEmpty();
+
+ GetByDocumentIdRequest getByDocumentIdRequest =
+ new GetByDocumentIdRequest.Builder("namespace")
+ .addIds("id1")
+ .build();
+ List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
+ assertThat(outDocuments).hasSize(1);
+ assertThat(outDocuments).containsExactly(email);
+ }
+
+ @Test
+ public void testSetSchema_publiclyVisible_unsupported() {
+ assumeFalse(mDb1.getFeatures()
+ .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("Email").build())
+ .setPubliclyVisibleSchema("Email",
+ new PackageIdentifier(mContext.getPackageName(), new byte[32])).build();
+ Exception e = assertThrows(UnsupportedOperationException.class,
+ () -> mDb1.setSchemaAsync(request).get());
+ assertThat(e.getMessage()).isEqualTo("Publicly visible schema are not supported on this "
+ + "AppSearch implementation.");
+ }
+
+ @Test
+ public void testSetSchema_visibleToConfig() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+ byte[] cert1 = new byte[32];
+ byte[] cert2 = new byte[32];
+ Arrays.fill(cert1, (byte) 1);
+ Arrays.fill(cert2, (byte) 2);
+ PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1);
+ PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2);
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .setPubliclyVisibleTargetPackage(pkg1)
+ .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .setPubliclyVisibleTargetPackage(pkg2)
+ .addRequiredPermissions(ImmutableSet.of(3, 4)).build();
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemaTypeVisibleToConfig("builtin:Email", config1)
+ .addSchemaTypeVisibleToConfig("builtin:Email", config2)
+ .build();
+ mDb1.setSchemaAsync(request).get();
+
+ GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+ assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs())
+ .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2)));
+ }
+
+ @Test
+ public void testSetSchema_visibleToConfig_unsupported() {
+ assumeFalse(mDb1.getFeatures()
+ .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+
+ SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+ .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(new AppSearchSchema.Builder("Email").build())
+ .addSchemaTypeVisibleToConfig("Email", config).build();
+ Exception e = assertThrows(UnsupportedOperationException.class,
+ () -> mDb1.setSchemaAsync(request).get());
+ assertThat(e.getMessage()).isEqualTo("Schema visible to config are not supported on"
+ + " this AppSearch implementation.");
+ }
+
+ @Test
public void testGetSchema_longPropertyIndexingType() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
@@ -1112,9 +1564,106 @@
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
}
+
+ @Test
+ public void testPutDocuments_takenActions() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addDocumentClasses(SearchAction.class, ClickAction.class)
+ .build())
+ .get();
+
+ // Put a SearchAction and ClickAction document
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+ .setDocumentTtlMillis(0)
+ .setQuery("query")
+ .setFetchedResultCount(10)
+ .build();
+ ClickAction clickAction =
+ new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */2000)
+ .setDocumentTtlMillis(0)
+ .setQuery("query")
+ .setReferencedQualifiedId("pkg$db/ns#refId")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(3)
+ .setTimeStayOnResultMillis(1024)
+ .build();
+
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addTakenActions(searchAction, clickAction)
+ .build()));
+ assertThat(result.getSuccesses()).containsEntry("search", null);
+ assertThat(result.getSuccesses()).containsEntry("click", null);
+ assertThat(result.getFailures()).isEmpty();
+ }
// @exportToFramework:endStrip()
@Test
+ public void testPutDocuments_takenActionGenericDocuments() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // Schema registration
+ AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+ .addProperty(new LongPropertyConfig.Builder("actionType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("query")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+ .addProperty(new LongPropertyConfig.Builder("actionType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("query")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build()
+ ).build();
+
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(searchActionSchema, clickActionSchema)
+ .build()).get();
+
+ // Put search action and click action generic documents.
+ GenericDocument searchAction =
+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+ .setPropertyString("query", "body")
+ .build();
+ GenericDocument clickAction =
+ new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+ .setPropertyString("query", "body")
+ .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId")
+ .build();
+
+ AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addTakenActionGenericDocuments(searchAction, clickAction)
+ .build()));
+ assertThat(result.getSuccesses()).containsEntry("search", null);
+ assertThat(result.getSuccesses()).containsEntry("click", null);
+ assertThat(result.getFailures()).isEmpty();
+ }
+
+ @Test
public void testUpdateSchema() throws Exception {
// Schema registration
AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
@@ -2112,6 +2661,247 @@
assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
}
+// @exportToFramework:startStrip()
+ @Test
+ public void testQueryRankByClickActions_useTakenAction() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addDocumentClasses(SearchAction.class, ClickAction.class)
+ .build())
+ .get();
+
+ // Index several email documents
+ AppSearchEmail inEmail1 =
+ new AppSearchEmail.Builder("namespace", "email1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(1)
+ .build();
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "email2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(1)
+ .build();
+
+ String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+ mContext.getPackageName(), DB_NAME_1, inEmail1);
+ String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+ mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+ .setDocumentTtlMillis(0)
+ .setQuery("body")
+ .setFetchedResultCount(20)
+ .build();
+ ClickAction clickAction1 =
+ new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+ .setDocumentTtlMillis(0)
+ .setQuery("body")
+ .setReferencedQualifiedId(qualifiedId1)
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(1)
+ .setTimeStayOnResultMillis(512)
+ .build();
+ ClickAction clickAction2 =
+ new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+ .setDocumentTtlMillis(0)
+ .setQuery("body")
+ .setReferencedQualifiedId(qualifiedId2)
+ .setResultRankInBlock(2)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(128)
+ .build();
+ ClickAction clickAction3 =
+ new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
+ .setDocumentTtlMillis(0)
+ .setQuery("body")
+ .setReferencedQualifiedId(qualifiedId1)
+ .setResultRankInBlock(2)
+ .setResultRankGlobal(2)
+ .setTimeStayOnResultMillis(256)
+ .build();
+
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(inEmail1, inEmail2)
+ .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3)
+ .build()));
+
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+ .setOrder(SearchSpec.ORDER_DESCENDING)
+ .addFilterDocumentClasses(ClickAction.class)
+ .build();
+
+ // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+ // documents returned. It does not affect the number of child documents that are scored.
+ JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+ .setNestedSearch("query:body", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .setMaxJoinedResultCount(0)
+ .build();
+
+ // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+ // query = "body".
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setOrder(SearchSpec.ORDER_DESCENDING)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ assertThat(sr).hasSize(2);
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+ assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+ assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+ }
+// @exportToFramework:endStrip()
+
+ @Test
+ public void testQueryRankByTakenActions_useTakenActionGenericDocument() throws Exception {
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+ .addProperty(new LongPropertyConfig.Builder("actionType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("query")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+ .addProperty(new LongPropertyConfig.Builder("actionType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("query")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build()
+ ).build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema)
+ .build())
+ .get();
+
+ // Index several email documents
+ AppSearchEmail inEmail1 =
+ new AppSearchEmail.Builder("namespace", "email1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(1)
+ .build();
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "email2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(1)
+ .build();
+
+ String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+ mContext.getPackageName(), DB_NAME_1, inEmail1);
+ String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+ mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+ GenericDocument searchAction =
+ new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
+ .setCreationTimestampMillis(1000)
+ .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+ .setPropertyString("query", "body")
+ .build();
+ GenericDocument clickAction1 =
+ new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+ .setCreationTimestampMillis(2000)
+ .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+ .setPropertyString("query", "body")
+ .setPropertyString("referencedQualifiedId", qualifiedId1)
+ .build();
+ GenericDocument clickAction2 =
+ new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
+ .setCreationTimestampMillis(3000)
+ .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+ .setPropertyString("query", "body")
+ .setPropertyString("referencedQualifiedId", qualifiedId2)
+ .build();
+ GenericDocument clickAction3 =
+ new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
+ .setCreationTimestampMillis(4000)
+ .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+ .setPropertyString("query", "body")
+ .setPropertyString("referencedQualifiedId", qualifiedId1)
+ .build();
+
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(inEmail1, inEmail2)
+ .addTakenActionGenericDocuments(
+ searchAction, clickAction1, clickAction2, clickAction3)
+ .build()));
+
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+ .setOrder(SearchSpec.ORDER_DESCENDING)
+ .addFilterSchemas("builtin:ClickAction")
+ .build();
+
+ // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+ // documents returned. It does not affect the number of child documents that are scored.
+ JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+ .setNestedSearch("query:body", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .setMaxJoinedResultCount(0)
+ .build();
+
+ // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+ // query = "body".
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setOrder(SearchSpec.ORDER_DESCENDING)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ assertThat(sr).hasSize(2);
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+ assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+ assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+ }
+
@Test
public void testQuery_invalidAdvancedRanking() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(
@@ -2267,7 +3057,7 @@
assertThat(documents).hasSize(1);
assertThat(documents).containsExactly(inDoc);
- // Query only for non-exist type
+ // Query only for non-existent type
searchResults = mDb1.search("body", new SearchSpec.Builder()
.addFilterSchemas("nonExistType")
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
@@ -2353,7 +3143,7 @@
assertThat(documents).hasSize(1);
assertThat(documents).containsExactly(expectedEmail);
- // Query only for non-exist namespace
+ // Query only for non-existent namespace
searchResults = mDb1.search("body",
new SearchSpec.Builder()
.addFilterNamespaces("nonExistNamespace")
@@ -2699,7 +3489,7 @@
SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection(
- SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+ SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -2760,7 +3550,7 @@
// Query with type property paths {"*", []}
SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList())
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -2822,7 +3612,7 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection("NonExistentType", Collections.emptyList())
.addProjection(
- SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+ SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -2842,6 +3632,23 @@
}
@Test
+ public void testSearchSpec_setSourceTag_notSupported() {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG));
+ // UnsupportedOperationException will be thrown with these queries so no need to
+ // define a schema and index document.
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build();
+
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("\"Hello, world!\"", searchSpec));
+ assertThat(exception).hasMessageThat().contains(
+ Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
public void testQuery_twoInstances() throws Exception {
// Schema registration
mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -2889,6 +3696,332 @@
}
@Test
+ public void testQuery_typePropertyFilters() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email1 =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ AppSearchEmail email2 =
+ new AppSearchEmail.Builder("namespace", "id2")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example subject with some body")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1, email2).build()));
+
+ // Query with type property filters {"Email", ["subject", "to"]}
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // Only email2 should be returned because email1 doesn't have the term "body" in subject
+ // or to fields
+ assertThat(documents).containsExactly(email2);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build())
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("namespace", "id2", "Note")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, note).build()));
+
+ // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
+ // schema has body in its property filter but Email schema doesn't.
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+ .addFilterProperties("Note", ImmutableList.of("body"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // Only the note document should be returned because the email property filter doesn't
+ // allow searching in the body.
+ assertThat(documents).containsExactly(note);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build())
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example subject with some body")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("namespace", "id2", "Note")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, note).build()));
+
+ // Query with type property paths {"*": ["subject", "title"]}
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+ ImmutableList.of("subject", "title"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // The wildcard property filter will apply to both the Email and Note schema. The email
+ // document should be returned since it has the term "body" in its subject property. The
+ // note document should not be returned since it doesn't have the term "body" in the title
+ // property (subject property is not applicable for Note schema)
+ assertThat(documents).containsExactly(email);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build())
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example subject with some body")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("namespace", "id2", "Note")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, note).build()));
+
+ // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+ ImmutableList.of("subject", "title"))
+ .addFilterProperties("Note", ImmutableList.of("body"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // The wildcard property filter will only apply to the Email schema since Note schema has
+ // its own explicit property filter specified. The email document should be returned since
+ // it has the term "body" in its subject property. The note document should also be returned
+ // since it has the term "body" in the body property.
+ assertThat(documents).containsExactly(email, note);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build())
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example subject with some body")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("namespace", "id2", "Note")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, note).build()));
+
+ // Query with type property paths {"NonExistentType": ["to", "title"]}
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // The supplied property filters don't apply to either schema types. Both the documents
+ // should be returned since the term "body" is present in at least one of their properties.
+ assertThat(documents).containsExactly(email, note);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersEmpty() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("Note")
+ .addProperty(new StringPropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build())
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("namespace", "id2", "Note")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email, note).build()));
+
+ // Query with type property paths {"email": []}
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ // The email document should not be returned since the property filter doesn't allow
+ // searching any property.
+ assertThat(documents).containsExactly(note);
+ }
+
+ @Test
public void testSnippet() throws Exception {
// Schema registration
AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
@@ -2929,10 +4062,10 @@
assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
+ "Another nonsense word that’s used a lot is bar");
assertThat(matchInfo.getExactMatchRange()).isEqualTo(
- new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
+ new SearchResult.MatchRange(/*start=*/29, /*end=*/32));
assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
assertThat(matchInfo.getSnippetRange()).isEqualTo(
- new SearchResult.MatchRange(/*lower=*/26, /*upper=*/33));
+ new SearchResult.MatchRange(/*start=*/26, /*end=*/33));
assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
if (!mDb1.getFeatures().isFeatureSupported(
@@ -2941,7 +4074,7 @@
assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
} else {
assertThat(matchInfo.getSubmatchRange()).isEqualTo(
- new SearchResult.MatchRange(/*lower=*/29, /*upper=*/31));
+ new SearchResult.MatchRange(/*start=*/29, /*end=*/31));
assertThat(matchInfo.getSubmatch()).isEqualTo("fo");
}
}
@@ -3068,7 +4201,7 @@
SearchResult.MatchInfo matchInfo = matchInfos.get(0);
assertThat(matchInfo.getFullText()).isEqualTo(japanese);
assertThat(matchInfo.getExactMatchRange()).isEqualTo(
- new SearchResult.MatchRange(/*lower=*/44, /*upper=*/45));
+ new SearchResult.MatchRange(/*start=*/44, /*end=*/45));
assertThat(matchInfo.getExactMatch()).isEqualTo("は");
if (!mDb1.getFeatures().isFeatureSupported(
@@ -3077,7 +4210,7 @@
assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
} else {
assertThat(matchInfo.getSubmatchRange()).isEqualTo(
- new SearchResult.MatchRange(/*lower=*/44, /*upper=*/45));
+ new SearchResult.MatchRange(/*start=*/44, /*end=*/45));
assertThat(matchInfo.getSubmatch()).isEqualTo("は");
}
}
@@ -3992,7 +5125,7 @@
// returned.
SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail4);
@@ -4001,7 +5134,7 @@
// be returned ('email4' and 'email2').
searchResults = mDb1.search("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
.build());
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail4, inEmail2);
@@ -4011,13 +5144,442 @@
searchResults = mDb1.search("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
.build());
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail4, inEmail2);
}
@Test
+ public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+ // Schema registration
+ AppSearchSchema genericSchema =
+ new AppSearchSchema.Builder("Generic")
+ .addProperty(
+ new StringPropertyConfig.Builder("foo")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(genericSchema)
+ .build())
+ .get();
+
+ // Index four documents.
+ AppSearchEmail inEmail1 =
+ new AppSearchEmail.Builder("namespace1", "id1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace1", "id2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+ AppSearchEmail inEmail3 =
+ new AppSearchEmail.Builder("namespace2", "id3")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+ AppSearchEmail inEmail4 =
+ new AppSearchEmail.Builder("namespace2", "id4")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+ AppSearchEmail inEmail5 =
+ new AppSearchEmail.Builder("namespace2", "id5")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
+ GenericDocument inDoc1 =
+ new GenericDocument.Builder<>("namespace3", "id6", "Generic")
+ .setPropertyString("foo", "body")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
+ GenericDocument inDoc2 =
+ new GenericDocument.Builder<>("namespace3", "id7", "Generic")
+ .setPropertyString("foo", "body")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
+ GenericDocument inDoc3 =
+ new GenericDocument.Builder<>("namespace4", "id8", "Generic")
+ .setPropertyString("foo", "body")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
+
+ // Query with per package result grouping. Only the last document 'doc3' should be
+ // returned.
+ SearchResults searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3);
+
+ // Query with per namespace result grouping. Only the last document in each namespace should
+ // be returned ('doc3', 'doc2', 'email5' and 'email2').
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+ /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+ // Query with per namespace result grouping. Two of the last documents in each namespace
+ // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+ /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents)
+ .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+ // Query with per schema result grouping. Only the last document of each schema type should
+ // be returned ('doc3', 'email5')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+ // Query with per schema result grouping. Only the last two documents of each schema type
+ // should be returned ('doc3', 'doc2', 'email5', 'email4')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+ // Query with per package and per namespace result grouping. Only the last document in each
+ // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+ // Query with per package and per namespace result grouping. Only the last two documents
+ // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+ // 'email2', 'email1')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents)
+ .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+ // Query with per package and per schema type result grouping. Only the last document in
+ // each schema type should be returned. ('doc3', 'email5')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_SCHEMA
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+ // Query with per package and per schema type result grouping. Only the last two document in
+ // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_SCHEMA
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+ // Query with per namespace and per schema type result grouping. Only the last document in
+ // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+ /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+ // Query with per namespace and per schema type result grouping. Only the last two documents
+ // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+ // 'email2', 'email1')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+ /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents)
+ .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+ // Query with per namespace, per package and per schema type result grouping. Only the last
+ // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 1)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+ // Query with per namespace, per package and per schema type result grouping. Only the last
+ // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
+ // 'email4', 'email2', 'email1')
+ searchResults =
+ mDb1.search(
+ "body",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 2)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents)
+ .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+ }
+
+ @Test
+ public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
+ assumeFalse(
+ mDb1.getFeatures()
+ .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+ .get();
+
+ // Index four documents.
+ AppSearchEmail inEmail1 =
+ new AppSearchEmail.Builder("namespace1", "id1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace1", "id2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+ AppSearchEmail inEmail3 =
+ new AppSearchEmail.Builder("namespace2", "id3")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+ AppSearchEmail inEmail4 =
+ new AppSearchEmail.Builder("namespace2", "id4")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+ // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+ // UnsupportedOperationException will be thrown.
+ SearchSpec searchSpec1 =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+ .build();
+ UnsupportedOperationException exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("body", searchSpec1));
+ assertThat(exception)
+ .hasMessageThat()
+ .contains(
+ Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+ + " is not available on this"
+ + " AppSearch implementation.");
+
+ // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+ // UnsupportedOperationException will be thrown.
+ SearchSpec searchSpec2 =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_PACKAGE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+ /* limit= */ 1)
+ .build();
+ exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("body", searchSpec2));
+ assertThat(exception)
+ .hasMessageThat()
+ .contains(
+ Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+ + " is not available on this"
+ + " AppSearch implementation.");
+
+ // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+ // UnsupportedOperationException will be thrown.
+ SearchSpec searchSpec3 =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+ /* limit= */ 1)
+ .build();
+ exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("body", searchSpec3));
+ assertThat(exception)
+ .hasMessageThat()
+ .contains(
+ Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+ + " is not available on this"
+ + " AppSearch implementation.");
+
+ // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+ // UnsupportedOperationException will be thrown.
+ SearchSpec searchSpec4 =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setResultGrouping(
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+ /* limit= */ 1)
+ .build();
+ exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("body", searchSpec4));
+ assertThat(exception)
+ .hasMessageThat()
+ .contains(
+ Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+ + " is not available on this"
+ + " AppSearch implementation.");
+ }
+
+ @Test
public void testIndexNestedDocuments() throws Exception {
// Schema registration
mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -4308,8 +5870,6 @@
.build();
mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
- // ListFilterQueryLanguage is enabled so that EXPERIMENTAL_ICING_ADVANCED_QUERY gets enabled
- // in IcingLib.
// Disable VERBATIM_SEARCH in the SearchSpec.
SearchResults searchResults = mDb1.search("\"Hello, world!\"",
new SearchSpec.Builder()
@@ -4507,6 +6067,138 @@
}
@Test
+ public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception {
+ assumeFalse(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+
+ // UnsupportedOperationException will be thrown with these queries so no need to
+ // define a schema and index document.
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build();
+
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("\"Hello, world!\"", searchSpec));
+ assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
+ public void testQuery_hasPropertyFunction() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+ .addProperty(new StringPropertyConfig.Builder("prop1")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("prop2")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).addSchemas(schema).build()).get();
+
+ GenericDocument doc1 = new GenericDocument.Builder<>(
+ "namespace", "id1", "Schema")
+ .setPropertyString("prop1", "Hello, world!")
+ .build();
+ GenericDocument doc2 = new GenericDocument.Builder<>(
+ "namespace", "id2", "Schema")
+ .setPropertyString("prop2", "Hello, world!")
+ .build();
+ GenericDocument doc3 = new GenericDocument.Builder<>(
+ "namespace", "id3", "Schema")
+ .setPropertyString("prop1", "Hello, world!")
+ .setPropertyString("prop2", "Hello, world!")
+ .build();
+ mDb1.putAsync(new PutDocumentsRequest.Builder()
+ .addGenericDocuments(doc1, doc2, doc3).build()).get();
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build();
+ SearchResults searchResults = mDb1.search("hasProperty(\"prop1\")",
+ searchSpec);
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(2);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+ assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+ searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec);
+ page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(2);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+ assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+
+ searchResults = mDb1.search(
+ "hasProperty(\"prop1\") AND hasProperty(\"prop2\")",
+ searchSpec);
+ page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+ }
+
+ @Test
+ public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+ .addProperty(new StringPropertyConfig.Builder("prop")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).addSchemas(schema).build()).get();
+
+ GenericDocument doc = new GenericDocument.Builder<>(
+ "namespace1", "id1", "Schema")
+ .setPropertyString("prop", "Hello, world!")
+ .build();
+ mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
+
+ // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the
+ // SearchSpec.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(false)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search("hasProperty(\"prop\")",
+ searchSpec);
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> searchResults.getNextPageAsync().get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) executionException.getCause();
+ assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+ assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+ assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
+
+ // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec.
+ searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(false)
+ .build();
+ SearchResults searchResults2 = mDb1.search("hasProperty(\"prop\")",
+ searchSpec);
+ executionException = assertThrows(ExecutionException.class,
+ () -> searchResults2.getNextPageAsync().get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ exception = (AppSearchException) executionException.getCause();
+ assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+ assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+ assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION");
+ }
+
+ @Test
public void testQuery_propertyWeightsNotSupported() throws Exception {
assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
@@ -4793,6 +6485,257 @@
}
@Test
+ public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // A full example of how join might be used with property filters in join spec
+ AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+ .addProperty(new StringPropertyConfig.Builder("entityId")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("note")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("viewType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+ .build()).get();
+
+ // Index 2 email documents
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "id2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+
+ // Index 2 viewAction documents, one for email1 and the other for email2
+ String qualifiedId1 =
+ DocumentIdUtil.createQualifiedId(
+ ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+ "namespace", "id1");
+ String qualifiedId2 =
+ DocumentIdUtil.createQualifiedId(
+ ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+ "namespace", "id2");
+ GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+ .setPropertyString("entityId", qualifiedId1)
+ .setPropertyString("note", "Viewed email on Monday")
+ .setPropertyString("viewType", "Stared").build();
+ GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+ .setPropertyString("entityId", qualifiedId2)
+ .setPropertyString("note", "Viewed email on Tuesday")
+ .setPropertyString("viewType", "Viewed").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+ viewAction1, viewAction2)
+ .build()));
+
+ // The nested search spec only allows searching the viewType property for viewAction
+ // schema type. It also specifies a property filter for Email schema.
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
+ ImmutableList.of("subject"))
+ .build();
+
+ // Search for the term "Viewed" in join spec
+ JoinSpec js = new JoinSpec.Builder("entityId")
+ .setNestedSearch("Viewed", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .build();
+
+ SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ // Both email docs are returned, email2 comes first because it has higher number of
+ // joined documents. The property filters for Email schema specified in the nested search
+ // specs don't apply to the outer query (otherwise none of the email documents would have
+ // been returned).
+ assertThat(sr).hasSize(2);
+
+ // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+ // the join spec, so it should be present in the joined results.
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+ assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+ assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+ // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
+ // in the join spec, so it should not be present in the joined results.
+ assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+ assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+ assertThat(sr.get(1).getJoinedResults()).isEmpty();
+ }
+
+ @Test
+ public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // A full example of how join might be used with property filters in join spec
+ AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+ .addProperty(new StringPropertyConfig.Builder("entityId")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("note")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("viewType")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+ .build()).get();
+
+ // Index 2 email documents
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "id2")
+ .setFrom("[email protected]")
+ .setTo("[email protected]", "[email protected]")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+
+ // Index 2 viewAction documents, one for email1 and the other for email2
+ String qualifiedId1 =
+ DocumentIdUtil.createQualifiedId(
+ ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+ "namespace", "id1");
+ String qualifiedId2 =
+ DocumentIdUtil.createQualifiedId(
+ ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+ "namespace", "id2");
+ GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+ .setPropertyString("entityId", qualifiedId1)
+ .setPropertyString("note", "Viewed email on Monday")
+ .setPropertyString("viewType", "Stared").build();
+ GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+ .setPropertyString("entityId", qualifiedId2)
+ .setPropertyString("note", "Viewed email on Tuesday")
+ .setPropertyString("viewType", "Viewed").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+ viewAction1, viewAction2)
+ .build()));
+
+ // The nested search spec doesn't specify any property filters.
+ SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+
+ // Search for the term "Viewed" in join spec
+ JoinSpec js = new JoinSpec.Builder("entityId")
+ .setNestedSearch("Viewed", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .build();
+
+ // Outer search spec adds property filters for both Email and ViewAction schema
+ SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
+ .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ // Both email docs are returned as they both satisfy the property filters for Email, email2
+ // comes first because it has higher id lexicographically.
+ assertThat(sr).hasSize(2);
+
+ // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+ // the outer spec (although those property filters are irrelevant for joined documents),
+ // it should be present in the joined results.
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+ assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+ assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+ // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
+ // in the outer spec, but property filters in the outer spec should not apply on joined
+ // documents, so viewAction1 should be present in the joined results.
+ assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+ assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+ assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+ assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+ }
+
+ @Test
+ public void testQuery_typePropertyFiltersNotSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
+ // exception is thrown
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+ .build();
+ UnsupportedOperationException exception =
+ assertThrows(UnsupportedOperationException.class,
+ () -> mDb1.search("body", searchSpec));
+ assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
public void testSimpleJoin() throws Exception {
assumeTrue(mDb1.getFeatures()
.isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
@@ -4915,7 +6858,7 @@
assertThrows(UnsupportedOperationException.class, () ->
mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get());
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get());
}
@Test
@@ -4960,14 +6903,14 @@
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultOne, resultTwo, resultThree, resultFour)
.inOrder();
// Query first 2 suggestions, and they will be ranked.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get();
assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
}
@@ -5008,21 +6951,21 @@
// namespace1 has 2 results.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace1").build()).get();
assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
// namespace2 has 1 result.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace2").build()).get();
assertThat(suggestions).containsExactly(resultFoo).inOrder();
// namespace2 and 3 has 2 results.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace2", "namespace3")
.build()).get();
assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5030,7 +6973,7 @@
// non exist namespace has empty result
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("nonExistNamespace").build()).get();
assertThat(suggestions).isEmpty();
}
@@ -5077,7 +7020,7 @@
// Only search for namespace1/doc1
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace1")
.addFilterDocumentIds("namespace1", "id1")
.build()).get();
@@ -5086,7 +7029,7 @@
// Only search for namespace1/doc1 and namespace1/doc2
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace1")
.addFilterDocumentIds("namespace1", ImmutableList.of("id1", "id2"))
.build()).get();
@@ -5095,7 +7038,7 @@
// Only search for namespace1/doc1 and namespace2/doc3
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterNamespaces("namespace1", "namespace2")
.addFilterDocumentIds("namespace1", "id1")
.addFilterDocumentIds("namespace2", ImmutableList.of("id3"))
@@ -5105,7 +7048,7 @@
// Only search for namespace1/doc1 and everything in namespace2
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterDocumentIds("namespace1", "id1")
.build()).get();
assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
@@ -5163,21 +7106,21 @@
// Type1 has 2 results.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterSchemas("Type1").build()).get();
assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
// Type2 has 1 result.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterSchemas("Type2").build()).get();
assertThat(suggestions).containsExactly(resultFoo).inOrder();
// Type2 and 3 has 2 results.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterSchemas("Type2", "Type3")
.build()).get();
assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5185,7 +7128,7 @@
// non exist type has empty result.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.addFilterSchemas("nonExistType").build()).get();
assertThat(suggestions).isEmpty();
}
@@ -5233,13 +7176,13 @@
// prefix f has 2 results.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultFoo, resultFool);
// prefix b has 2 results.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"b",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultBar, resultBaz);
}
@@ -5285,7 +7228,7 @@
// rank by NONE, the order should be arbitrary but all terms appear.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.setRankingStrategy(SearchSuggestionSpec
.SUGGESTION_RANKING_STRATEGY_NONE)
.build()).get();
@@ -5294,7 +7237,7 @@
// rank by document count, the order should be term1:3 > term2:2 > term3:1
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.setRankingStrategy(SearchSuggestionSpec
.SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
.build()).get();
@@ -5303,7 +7246,7 @@
// rank by term frequency, the order should be term3:5 > term2:4 > term1:3
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
.setRankingStrategy(SearchSuggestionSpec
.SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
.build()).get();
@@ -5348,7 +7291,7 @@
// prefix t has 3 results.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
// Delete the document
@@ -5359,7 +7302,7 @@
// now prefix t has 2 results.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultThree, resultTart);
}
@@ -5397,7 +7340,7 @@
// prefix t has 3 results.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
// replace the document
@@ -5411,7 +7354,7 @@
// prefix t has 2 results for now.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultThree, resultTwist);
}
@@ -5448,13 +7391,13 @@
// database 1 could get suggestion results
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
// database 2 couldn't get suggestion results
suggestions = mDb2.searchSuggestionAsync(
/*suggestionQueryExpression=*/"t",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
assertThat(suggestions).isEmpty();
}
@@ -5488,7 +7431,7 @@
// Search "bar AND f" only document 1 should match the search.
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"bar f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
SearchSuggestionResult barFo =
new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
assertThat(suggestions).containsExactly(barFo);
@@ -5497,7 +7440,7 @@
// match.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"bar OR cat f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
SearchSuggestionResult barCatFo =
new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
SearchSuggestionResult barCatFoo =
@@ -5507,7 +7450,7 @@
// Search for "(bar AND cat) OR f", all documents could match.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"(bar cat) OR f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
SearchSuggestionResult barCatOrFo =
new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
SearchSuggestionResult barCatOrFoo =
@@ -5520,7 +7463,7 @@
// Search for "-bar f", document2 "cat foo" could and document3 "fool" could match.
suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"-bar f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
SearchSuggestionResult noBarFoo =
new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
SearchSuggestionResult noBarFool =
@@ -5529,6 +7472,162 @@
}
@Test
+ public void testSearchSuggestion_propertyFilter() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+ // Schema registration
+ AppSearchSchema schemaType1 =
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new StringPropertyConfig.Builder("propertyone")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("propertytwo")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ AppSearchSchema schemaType2 =
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new StringPropertyConfig.Builder("propertythree")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("propertyfour")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
+ .get();
+
+ // Index documents
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Type1")
+ .setPropertyString("propertyone", "termone")
+ .setPropertyString("propertytwo", "termtwo")
+ .build();
+ GenericDocument doc2 =
+ new GenericDocument.Builder<>("namespace", "id2", "Type2")
+ .setPropertyString("propertythree", "termthree")
+ .setPropertyString("propertyfour", "termfour")
+ .build();
+
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
+
+ SearchSuggestionResult resultOne =
+ new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
+ SearchSuggestionResult resultTwo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
+ SearchSuggestionResult resultThree =
+ new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
+ SearchSuggestionResult resultFour =
+ new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
+
+ // Only search for type1/propertyone
+ List<SearchSuggestionResult> suggestions =
+ mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t",
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterSchemas("Type1")
+ .addFilterProperties(
+ "Type1", ImmutableList.of("propertyone"))
+ .build())
+ .get();
+ assertThat(suggestions).containsExactly(resultOne);
+
+ // Only search for type1/propertyone and type1/propertytwo
+ suggestions =
+ mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t",
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterSchemas("Type1")
+ .addFilterProperties(
+ "Type1",
+ ImmutableList.of("propertyone", "propertytwo"))
+ .build())
+ .get();
+ assertThat(suggestions).containsExactly(resultOne, resultTwo);
+
+ // Only search for type1/propertyone and type2/propertythree
+ suggestions =
+ mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t",
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterSchemas("Type1", "Type2")
+ .addFilterProperties(
+ "Type1", ImmutableList.of("propertyone"))
+ .addFilterProperties(
+ "Type2", ImmutableList.of("propertythree"))
+ .build())
+ .get();
+ assertThat(suggestions).containsExactly(resultOne, resultThree);
+
+ // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
+ suggestions =
+ mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t",
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterSchemas("Type1", "Type2")
+ .addFilterProperties(
+ "Type1", ImmutableList.of("propertyone"))
+ .addFilterPropertyPaths(
+ "Type2",
+ ImmutableList.of(new PropertyPath("propertyfour")))
+ .build())
+ .get();
+ assertThat(suggestions).containsExactly(resultOne, resultFour);
+
+ // Only search for type1/propertyone and everything in type2
+ suggestions =
+ mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t",
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterProperties(
+ "Type1", ImmutableList.of("propertyone"))
+ .build())
+ .get();
+ assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
+ }
+
+ @Test
+ public void testSearchSuggestion_propertyFilter_notSupported() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+
+ SearchSuggestionSpec searchSuggestionSpec =
+ new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+ .addFilterSchemas("Type1")
+ .addFilterProperties("Type1", ImmutableList.of("property"))
+ .build();
+
+ // Search suggest with type property filters {"Email", ["property"]} and verify that
+ // unsupported exception is thrown
+ UnsupportedOperationException exception =
+ assertThrows(UnsupportedOperationException.class,
+ () -> mDb1.searchSuggestionAsync(
+ /* suggestionQueryExpression= */ "t", searchSuggestionSpec).get());
+ assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
public void testSearchSuggestion_PropertyRestriction() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
@@ -5566,7 +7665,7 @@
// Search for "bar AND subject:f"
List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
/*suggestionQueryExpression=*/"bar subject:f",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
SearchSuggestionResult barSubjectFo =
new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
SearchSuggestionResult barSubjectFoo =
@@ -5609,6 +7708,13 @@
Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
assertThat(actual).hasSize(3);
assertThat(actual).isEqualTo(request.getSchemas());
+
+ // Check that calling getParentType() for the EmailMessage schema returns Email and Message
+ for (AppSearchSchema schema : actual) {
+ if (schema.getSchemaType().equals("EmailMessage")) {
+ assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+ }
+ }
}
@Test
@@ -5641,6 +7747,70 @@
}
@Test
+ public void testGetSchema_indexableNestedPropsList() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
+
+ AppSearchSchema personSchema =
+ new AppSearchSchema.Builder("Person")
+ .addProperty(
+ new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "worksFor", "Organization")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setShouldIndexNestedProperties(false)
+ .addIndexableNestedProperties(Collections.singleton("name"))
+ .build())
+ .build();
+ AppSearchSchema organizationSchema =
+ new AppSearchSchema.Builder("Organization")
+ .addProperty(
+ new StringPropertyConfig.Builder("name")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder("notes")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build())
+ .build();
+
+ SetSchemaRequest setSchemaRequest =
+ new SetSchemaRequest.Builder()
+ .addSchemas(personSchema, organizationSchema)
+ .build();
+ mDb1.setSchemaAsync(setSchemaRequest).get();
+
+ Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
+ assertThat(actual).hasSize(2);
+ assertThat(actual).isEqualTo(setSchemaRequest.getSchemas());
+
+ for (AppSearchSchema schema : actual) {
+ if (schema.getSchemaType().equals("Person")) {
+ for (PropertyConfig property : schema.getProperties()) {
+ if (property.getName().equals("worksFor")) {
+ assertThat(
+ ((DocumentPropertyConfig) property)
+ .getIndexableNestedProperties()).containsExactly("name");
+ }
+ }
+ }
+ }
+ }
+
+ @Test
public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
AppSearchSchema messageSchema =
@@ -6473,4 +8643,643 @@
assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch_simple() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+ "my_model_v1"),
+ new EmbeddingVector(
+ new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+ .build();
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setPropertyString("body", "bar")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+ // - document 1: -0.9 (embedding1)
+ EmbeddingVector searchEmbedding = new EmbeddingVector(
+ new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+ // Match documents that have embeddings with a similarity closer to 0 that is
+ // greater than -1.
+ //
+ // The matched embeddings for each doc are:
+ // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+ // - document 1: -0.9 (embedding1)
+ // The scoring expression for each doc will be evaluated as:
+ // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2
+ // - document 1: sum({-0.9}) + sum({}) = -0.9
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding)
+ .setRankingStrategy(
+ "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(2);
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2);
+ assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+ assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch_propertyRestriction() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+ "my_model_v1"),
+ new EmbeddingVector(
+ new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+ .build();
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setPropertyString("body", "bar")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+ // - document 1: -0.9 (embedding1)
+ EmbeddingVector searchEmbedding = new EmbeddingVector(
+ new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+ // Create a query similar as above but with a property restriction, which still matches
+ // document 0 and document 1 but the semantic score 0.3 should be removed from document 0.
+ //
+ // The matched embeddings for each doc are:
+ // - document 0: -0.5 (embedding1)
+ // - document 1: -0.9 (embedding1)
+ // The scoring expression for each doc will be evaluated as:
+ // - document 0: sum({-0.5}) = -0.5
+ // - document 1: sum({-0.9}) = -0.9
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding)
+ .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "embedding1:semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(2);
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5);
+ assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+ assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+ "my_model_v1"),
+ new EmbeddingVector(
+ new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+ .build();
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setPropertyString("body", "bar")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+ // - document 1: -0.9 (embedding1)
+ EmbeddingVector searchEmbedding1 = new EmbeddingVector(
+ new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: -0.5 (embedding2)
+ // - document 1: -2.1 (embedding2)
+ EmbeddingVector searchEmbedding2 = new EmbeddingVector(
+ new float[]{-1, -1, 1}, "my_model_v2");
+
+ // Create a complex query that matches all hits from all documents.
+ //
+ // The matched embeddings for each doc are:
+ // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2)
+ // - document 1: -0.9 (embedding1), -2.1 (embedding2)
+ // The scoring expression for each doc will be evaluated as:
+ // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7
+ // - document 1: sum({-0.9}) + sum({-2.1}) = -3
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding1, searchEmbedding2)
+ .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0))) + "
+ + "sum(this.matchedSemanticScores(getSearchSpecEmbedding(1)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "semanticSearch(getSearchSpecEmbedding(0)) OR "
+ + "semanticSearch(getSearchSpecEmbedding(1))", searchSpec);
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(2);
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7);
+ assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+ assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch_hybrid() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+ "my_model_v1"),
+ new EmbeddingVector(
+ new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+ .build();
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setPropertyString("body", "bar")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: -0.5 (embedding2)
+ // - document 1: -2.1 (embedding2)
+ EmbeddingVector searchEmbedding = new EmbeddingVector(
+ new float[]{-1, -1, 1}, "my_model_v2");
+
+ // Create a hybrid query that matches document 0 because of term-based search
+ // and document 1 because of embedding-based search.
+ //
+ // The matched embeddings for each doc are:
+ // - document 1: -2.1 (embedding2)
+ // The scoring expression for each doc will be evaluated as:
+ // - document 0: sum({}) = 0
+ // - document 1: sum({-2.1}) = -2.1
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding)
+ .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "foo OR semanticSearch(getSearchSpecEmbedding(0), -10, -1)", searchSpec);
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(2);
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
+ assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+ assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearchWithoutEnablingFeatureFails() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding1", new EmbeddingVector(
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+ .setPropertyEmbedding("embedding2", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+ "my_model_v1"),
+ new EmbeddingVector(
+ new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+
+ EmbeddingVector searchEmbedding = new EmbeddingVector(
+ new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding)
+ .setRankingStrategy(
+ "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .setListFilterQueryLanguageEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> searchResults.getNextPageAsync().get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) executionException.getCause();
+ assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+ assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+ assertThat(exception).hasMessageThat().contains("EMBEDDING_SEARCH");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch_notSupported() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ assumeFalse(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec));
+ assertThat(exception).hasMessageThat().contains(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public void testTokenizeSearch_simple() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .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();
+
+ // Index documents
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setPropertyString("body", "foo bar")
+ .setCreationTimestampMillis(1000)
+ .build();
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setPropertyString("body", "bar")
+ .setCreationTimestampMillis(1000)
+ .build();
+ GenericDocument doc2 =
+ new GenericDocument.Builder<>("namespace", "id2", "Email")
+ .setPropertyString("body", "foo")
+ .setCreationTimestampMillis(1000)
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1, doc2).build()));
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterTokenizeFunctionEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search("tokenize(\"foo.\")", searchSpec);
+ List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
+ assertThat(results).containsExactly(doc2, doc0);
+
+ searchResults = mDb1.search("tokenize(\"bar, foo\")", searchSpec);
+ results = convertSearchResultsToDocuments(searchResults);
+ assertThat(results).containsExactly(doc0);
+
+ searchResults = mDb1.search("tokenize(\"\\\"bar, \\\"foo\\\"\")", searchSpec);
+ results = convertSearchResultsToDocuments(searchResults);
+ assertThat(results).containsExactly(doc0);
+
+ searchResults = mDb1.search("tokenize(\"bar ) foo\")", searchSpec);
+ results = convertSearchResultsToDocuments(searchResults);
+ assertThat(results).containsExactly(doc0);
+
+ searchResults = mDb1.search("tokenize(\"bar foo(\")", searchSpec);
+ results = convertSearchResultsToDocuments(searchResults);
+ assertThat(results).containsExactly(doc0);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public void testTokenizeSearch_notSupported() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ assumeFalse(
+ mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterTokenizeFunctionEnabled(true)
+ .build();
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("tokenize(\"foo.\")", searchSpec));
+ assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_TOKENIZE_FUNCTION
+ + " is not available on this AppSearch implementation.");
+ }
+
+ @Test
+ @RequiresFlagsEnabled({
+ Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS,
+ Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
+ public void testInformationalRankingExpressions() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(
+ AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ final int doc0DocScore = 2;
+ GenericDocument doc0 =
+ new GenericDocument.Builder<>("namespace", "id0", "Email")
+ .setScore(doc0DocScore)
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding", new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+ .build();
+ final int doc1DocScore = 3;
+ GenericDocument doc1 =
+ new GenericDocument.Builder<>("namespace", "id1", "Email")
+ .setScore(doc1DocScore)
+ .setCreationTimestampMillis(1000)
+ .setPropertyEmbedding("embedding", new EmbeddingVector(
+ new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"),
+ new EmbeddingVector(
+ new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+ // Add an embedding search with dot product semantic scores:
+ // - document 0: 0.5
+ // - document 1: -0.9, 0.5
+ EmbeddingVector searchEmbedding = new EmbeddingVector(
+ new float[]{1, -1, -1, 1, -1}, "my_model");
+
+ // Make an embedding query that matches all documents.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(searchEmbedding)
+ .setRankingStrategy(
+ "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .addInformationalRankingExpressions(
+ "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+ .addInformationalRankingExpressions("this.documentScore()")
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ SearchResults searchResults = mDb1.search(
+ "semanticSearch(getSearchSpecEmbedding(0))", searchSpec);
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(2);
+ // doc0:
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+ assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5);
+ // doc0 has 1 embedding vector and a document score of 2.
+ assertThat(results.get(0).getInformationalRankingSignals())
+ .containsExactly(1.0, (double) doc0DocScore).inOrder();
+
+ // doc1:
+ assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+ assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5);
+ // doc1 has 2 embedding vectors and a document score of 3.
+ assertThat(results.get(1).getInformationalRankingSignals())
+ .containsExactly(2.0, (double) doc1DocScore).inOrder();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public void testInformationalRankingExpressions_notSupported() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setRankingStrategy("this.documentScore() + 1")
+ .addInformationalRankingExpressions("this.documentScore()")
+ .build();
+ UnsupportedOperationException exception = assertThrows(
+ UnsupportedOperationException.class,
+ () -> mDb1.search("foo", searchSpec));
+ assertThat(exception).hasMessageThat().contains(
+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+ + " are not available on this AppSearch implementation.");
+ }
+
+ @Test
+ public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
+ // 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();
+ mDb1.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(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.getPropertyBytesArray("bytes")).isEmpty();
+ assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
index 453d9225..6f08abf 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
@@ -18,24 +18,18 @@
package androidx.appsearch.cts.app;
import android.content.Context;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.playservicesstorage.PlayServicesStorage;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Assume;
-import org.junit.Test;
import java.util.concurrent.ExecutorService;
-// TODO(b/237116468): Remove SdkSuppress once AppSearchAttributionSource available for lower API
-// levels.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AppSearchSessionGmsCtsTest extends AppSearchSessionCtsTestBase {
private boolean mIsGmsAvailable;
@@ -68,13 +62,4 @@
super.tearDown();
}
}
-
- @Override
- @Test
- public void testRfc822_unsupportedFeature_throwsException() {
- // TODO(b/280463238): // TODO(b/280463238): KNOWN_ISSUE will be fixed in next
- // play-services-appsearch drop.
- // expected: tokenizerType is out of range of [0, 1] (too high)
- // but was : tokenizerType is out of range of [%d, %d] (too high) [0, 1]
- }
}
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 aba266a..79e7ad3 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
@@ -20,7 +20,6 @@
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;
@@ -34,7 +33,6 @@
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;
@@ -560,44 +558,4 @@
assertThat(result.getFailures().get("id1").getErrorMessage())
.contains("was too large to write. Max is 16777215");
}
-
- @Test
- public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
- Context context = ApplicationProvider.getApplicationContext();
- 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();
-
- // 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 0fdd512..f2fc0c2 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,9 +16,6 @@
// @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;
@@ -30,16 +27,9 @@
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.test.core.app.ApplicationProvider;
import androidx.test.filters.SdkSuppress;
@@ -93,51 +83,11 @@
}
@Test
+ @Override
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();
- }
+ // b/185441119 was fixed in Android T, this test will fail on S_V2 devices and below.
+ assumeTrue(Build.VERSION.SDK_INT >= 33);
+ super.testPutDocuments_emptyBytesAndDocuments();
}
@Override
@@ -202,5 +152,4 @@
@Override
@Test
public void testQuery_advancedRankingWithJoin() throws Exception { }
-
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
index 1d70497..b666e4d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
@@ -20,10 +20,18 @@
import static org.junit.Assert.assertThrows;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
+import org.junit.Rule;
import org.junit.Test;
+import java.util.Objects;
+
public class GenericDocumentCtsTest {
private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
@@ -36,6 +44,9 @@
.setCreationTimestampMillis(6789L)
.build();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
@SuppressWarnings("deprecation")
public void testMaxIndexedProperties() {
@@ -364,9 +375,31 @@
() -> builder.setPropertyString("testKey", "string1", nullString));
}
-// @exportToFramework:startStrip()
+ @Test
+ public void testDocumentInvalid_setNullByteValues() {
+ GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1");
+ byte[] nullBytes = null;
- // TODO(b/171882200): Expose this test in Android T
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setPropertyBytes("propBytes", new byte[][]{{1, 2}, nullBytes}));
+ }
+
+ @Test
+ public void testDocumentInvalid_setNullDocValues() {
+ GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1");
+ GenericDocument doc = new GenericDocument.Builder<>("namespace",
+ "id2",
+ "schemaType2").build();
+ GenericDocument nullDoc = null;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setPropertyDocument("propDocs", doc, nullDoc));
+ }
+
@Test
public void testDocument_toBuilder() {
GenericDocument document1 = new GenericDocument.Builder<>(
@@ -381,12 +414,13 @@
.build();
GenericDocument document2 =
new GenericDocument.Builder<>(document1)
- .setId("id2")
- .setNamespace("namespace2")
- .setPropertyBytes("byteKey1", sByteArray2)
- .setPropertyLong("longKey2", 10L)
- .clearProperty("booleanKey1")
- .build();
+ .setId("id2")
+ .setNamespace("namespace2")
+ .setSchemaType("schemaType2")
+ .setPropertyBytes("byteKey1", sByteArray2)
+ .setPropertyLong("longKey2", 10L)
+ .clearProperty("booleanKey1")
+ .build();
// Make sure old doc hasn't changed
assertThat(document1.getId()).isEqualTo("id1");
@@ -399,7 +433,7 @@
// Make sure the new doc contains the expected values
GenericDocument expectedDoc = new GenericDocument.Builder<>(
- "namespace2", "id2", "schemaType1")
+ "namespace2", "id2", "schemaType2")
.setCreationTimestampMillis(5L)
.setPropertyLong("longKey1", 1L, 2L, 3L)
.setPropertyLong("longKey2", 10L)
@@ -411,7 +445,51 @@
assertThat(document2).isEqualTo(expectedDoc);
}
-// @exportToFramework:endStrip()
+ @Test
+ public void testDocument_toBuilder_doesNotModifyOriginal() {
+ GenericDocument oldDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+ .setScore(42)
+ .setPropertyString("propString", "Hello")
+ .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+ .setPropertyDocument(
+ "propDocument",
+ new GenericDocument.Builder<>("namespace", "id2", "schema2")
+ .setPropertyString("propString", "Goodbye")
+ .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+ .build())
+ .build();
+
+ GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
+ .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+ .setPropertyDocument(
+ "propDocument",
+ new GenericDocument.Builder<>("namespace", "id3", "schema3")
+ .setPropertyString("propString", "Bye")
+ .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
+ .build())
+ .build();
+
+ // Check that the original GenericDocument is unmodified.
+ assertThat(oldDoc.getScore()).isEqualTo(42);
+ assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
+ assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+ assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+ .isEqualTo("Goodbye");
+ assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+ .isEqualTo(new byte[][]{{3, 4}});
+
+ // Check that the new GenericDocument has modified the original fields correctly.
+ assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+ assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+ .isEqualTo("Bye");
+ assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+ .isEqualTo(new byte[][]{{5, 6}});
+
+ // Check that the new GenericDocument copies fields that aren't set.
+ assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
+ assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
+ "propString"));
+ }
@Test
public void testRetrieveTopLevelProperties() {
@@ -886,4 +964,259 @@
assertThat(documents[1].getPropertyNames()).containsExactly("propString", "propInts",
"propIntsTwo");
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentEquals_identicalWithEmbeddingValues() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+ GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setTtlMillis(1L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .build();
+ GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setTtlMillis(1L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .build();
+ assertThat(document1).isEqualTo(document2);
+ assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentEquals_differentOrderWithEmbeddingValues() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+ GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+ .build();
+
+ // Create second document with same parameter but different order.
+ GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .build();
+ assertThat(document1).isEqualTo(document2);
+ assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentGetEmbeddingValue() {
+ EmbeddingVector embedding = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setScore(1)
+ .setTtlMillis(1L)
+ .setPropertyLong("longKey1", 1L)
+ .setPropertyDouble("doubleKey1", 1.0)
+ .setPropertyBoolean("booleanKey1", true)
+ .setPropertyString("stringKey1", "test-value1")
+ .setPropertyEmbedding("embeddingKey1", embedding)
+ .build();
+ assertThat(document.getId()).isEqualTo("id1");
+ assertThat(document.getTtlMillis()).isEqualTo(1L);
+ assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+ assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+ assertThat(document.getScore()).isEqualTo(1);
+ assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+ assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+ assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+ assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+ assertThat(Objects.requireNonNull(document.getPropertyEmbedding(
+ "embeddingKey1")).getValues()).usingExactEquality()
+ .containsExactly(1.1f, 2.2f, 3.3f).inOrder();
+ assertThat(Objects.requireNonNull(
+ document.getPropertyEmbedding("embeddingKey1")).getModelSignature()).isEqualTo(
+ "my_model_v1");
+
+ assertThat(document.getProperty("longKey1")).isInstanceOf(long[].class);
+ assertThat((long[]) document.getProperty("longKey1")).asList().containsExactly(1L);
+ assertThat(document.getProperty("doubleKey1")).isInstanceOf(double[].class);
+ assertThat((double[]) document.getProperty("doubleKey1")).usingTolerance(
+ 0.05).containsExactly(1.0);
+ assertThat(document.getProperty("booleanKey1")).isInstanceOf(boolean[].class);
+ assertThat((boolean[]) document.getProperty("booleanKey1")).asList().containsExactly(true);
+ assertThat(document.getProperty("stringKey1")).isInstanceOf(String[].class);
+ assertThat((String[]) document.getProperty("stringKey1")).asList().containsExactly(
+ "test-value1");
+ assertThat((EmbeddingVector[]) document.getProperty(
+ "embeddingKey1")).asList().containsExactly(embedding).inOrder();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentGetArrayEmbeddingValues() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .build();
+
+ assertThat(document.getId()).isEqualTo("id1");
+ assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+ assertThat(document.getPropertyLongArray("longKey1")).asList()
+ .containsExactly(1L, 2L, 3L).inOrder();
+ assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+ .containsExactly(1.0, 2.0, 3.0).inOrder();
+ assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
+ .containsExactly(true, false, true).inOrder();
+ assertThat(document.getPropertyStringArray("stringKey1")).asList()
+ .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
+ assertThat(document.getPropertyEmbeddingArray("embeddingKey1")).asList()
+ .containsExactly(embedding1, embedding2).inOrder();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocument_setEmptyEmbeddingValues() {
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+ .setPropertyBoolean("booleanKey")
+ .setPropertyString("stringKey")
+ .setPropertyBytes("byteKey")
+ .setPropertyDouble("doubleKey")
+ .setPropertyDocument("documentKey")
+ .setPropertyLong("longKey")
+ .setPropertyEmbedding("embeddingKey")
+ .build();
+ assertThat(document.getPropertyBooleanArray("booleanKey")).isEmpty();
+ assertThat(document.getPropertyStringArray("stringKey")).isEmpty();
+ assertThat(document.getPropertyBytesArray("byteKey")).isEmpty();
+ assertThat(document.getPropertyDoubleArray("doubleKey")).isEmpty();
+ assertThat(document.getPropertyDocumentArray("documentKey")).isEmpty();
+ assertThat(document.getPropertyLongArray("longKey")).isEmpty();
+ assertThat(document.getPropertyEmbeddingArray("embeddingKey")).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentInvalid_setNullEmbeddingValues() {
+ EmbeddingVector embedding = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+ GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+ "schemaType1");
+ EmbeddingVector nullEmbedding = null;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.setPropertyEmbedding("propEmbeddings",
+ new EmbeddingVector[]{embedding, nullEmbedding}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocument_toBuilderWithEmbeddingValues() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+ GenericDocument document1 = new GenericDocument.Builder<>(
+ /*namespace=*/"", "id1", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyBoolean("booleanKey1", true, false, true)
+ .setPropertyString("stringKey1", "String1", "String2", "String3")
+ .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+ .build();
+ GenericDocument document2 =
+ new GenericDocument.Builder<>(document1)
+ .setId("id2")
+ .setNamespace("namespace2")
+ .setPropertyEmbedding("embeddingKey1", embedding2)
+ .setPropertyLong("longKey2", 10L)
+ .clearProperty("booleanKey1")
+ .build();
+
+ // Make sure old doc hasn't changed
+ assertThat(document1.getId()).isEqualTo("id1");
+ assertThat(document1.getNamespace()).isEqualTo("");
+ assertThat(document1.getPropertyLongArray("longKey1")).asList()
+ .containsExactly(1L, 2L, 3L).inOrder();
+ assertThat(document1.getPropertyBooleanArray("booleanKey1")).asList()
+ .containsExactly(true, false, true).inOrder();
+ assertThat(document1.getPropertyLongArray("longKey2")).isNull();
+ assertThat(document1.getPropertyEmbeddingArray("embeddingKey1")).asList()
+ .containsExactly(embedding1, embedding2).inOrder();
+
+ // Make sure the new doc contains the expected values
+ GenericDocument expectedDoc = new GenericDocument.Builder<>(
+ "namespace2", "id2", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setPropertyLong("longKey1", 1L, 2L, 3L)
+ .setPropertyLong("longKey2", 10L)
+ .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+ .setPropertyString("stringKey1", "String1", "String2", "String3")
+ .setPropertyEmbedding("embeddingKey1", embedding2)
+ .build();
+ assertThat(document2).isEqualTo(expectedDoc);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testDocumentGetPropertyNamesWithEmbeddingValue() {
+ EmbeddingVector embedding = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+ GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setScore(1)
+ .setTtlMillis(1L)
+ .setPropertyLong("longKey1", 1L)
+ .setPropertyDouble("doubleKey1", 1.0)
+ .setPropertyBoolean("booleanKey1", true)
+ .setPropertyString("stringKey1", "test-value1")
+ .setPropertyEmbedding("embeddingKey1", embedding)
+ .build();
+ assertThat(document.getPropertyNames()).containsExactly("longKey1", "doubleKey1",
+ "booleanKey1", "stringKey1", "embeddingKey1");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingValuesCannotBeEmpty() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new EmbeddingVector(new float[]{}, "my_model"));
+ assertThat(exception).hasMessageThat().contains("Embedding values cannot be empty.");
+ }
}
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 405db5a..4d5a4f9 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
@@ -23,6 +23,7 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
import com.google.common.collect.ImmutableSet;
@@ -30,6 +31,7 @@
import org.junit.Test;
import java.util.Arrays;
+import java.util.Map;
public class GetSchemaResponseCtsTest {
@Test
@@ -76,10 +78,10 @@
ImmutableSet.of(packageIdentifier2))
.setRequiredPermissionsForSchemaTypeVisibility("Email2",
ImmutableSet.of(
- ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
- SetSchemaRequest.READ_EXTERNAL_STORAGE),
- ImmutableSet.of(SetSchemaRequest
- .READ_ASSISTANT_APP_SEARCH_DATA))
+ 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
@@ -156,6 +158,30 @@
.READ_ASSISTANT_APP_SEARCH_DATA)));
}
+
+ @Test
+ public void setVisibilityConfig() {
+ SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+ SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkg3", new byte[32]))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg4", new byte[32]))
+ .addRequiredPermissions(ImmutableSet.of(3, 4))
+ .build();
+
+ GetSchemaResponse getSchemaResponse =
+ new GetSchemaResponse.Builder().setVersion(42)
+ .setSchemaTypeVisibleToConfigs("Email",
+ ImmutableSet.of(visibilityConfig1, visibilityConfig2))
+ .build();
+
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).containsExactly("Email",
+ ImmutableSet.of(visibilityConfig1, visibilityConfig2));
+ }
+
@Test
public void getEmptyVisibility() {
GetSchemaResponse getSchemaResponse =
@@ -166,11 +192,19 @@
assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
}
+ @Test
+ public void getEmptyVisibility_visibilityConfig() {
+ GetSchemaResponse getSchemaResponse =
+ new GetSchemaResponse.Builder().setVersion(42)
+ .build();
+ assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).isEmpty();
+ }
+
// @exportToFramework:startStrip()
// Not exported as setVisibilitySettingSupported is hidden in framework
- /**
+ /**
* Makes sure an exception is thrown when visibility getters are called after visibility is set
- * to no supported.
+ * to no supported.
*/
@Test
public void setVisibility_setFalse() {
@@ -209,6 +243,14 @@
getSchemaResponse::getRequiredPermissionsForSchemaTypeVisibility);
assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+ " this backend/Android API level combination.");
+ e = assertThrows(UnsupportedOperationException.class,
+ getSchemaResponse::getPubliclyVisibleSchemas);
+ assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+ + " this backend/Android API level combination.");
+ e = assertThrows(UnsupportedOperationException.class,
+ getSchemaResponse::getSchemaTypesVisibleToConfigs);
+ assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+ + " this backend/Android API level combination.");
}
/**
@@ -248,4 +290,56 @@
original::getRequiredPermissionsForSchemaTypeVisibility);
}
// @exportToFramework:endStrip()
+
+ @Test
+ public void testVisibility_publicVisibility() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 1);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+ GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+ .setPubliclyVisibleSchema("Email1", packageIdentifier2)
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ .build();
+ assertThat(getSchemaResponse.getPubliclyVisibleSchemas().get("Email1"))
+ .isEqualTo(packageIdentifier1);
+ }
+
+ // @exportToFramework:startStrip()
+ // Not exported as setVisibilitySettingSupported is hidden in framework
+ @Test
+ public void testVisibility_publicVisibility_clearVisibility() {
+ byte[] sha256cert1 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ // This should clear all visibility settings.
+ .setVisibilitySettingSupported(true)
+ .build();
+
+ Map<String, PackageIdentifier> publiclyVisibleSchemas =
+ getSchemaResponse.getPubliclyVisibleSchemas();
+ assertThat(publiclyVisibleSchemas).isEmpty();
+ }
+
+ @Test
+ public void testVisibility_publicVisibility_notSupported() {
+ byte[] sha256cert1 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ .setVisibilitySettingSupported(false)
+ .build();
+
+ Exception e = assertThrows(UnsupportedOperationException.class,
+ getSchemaResponse::getPubliclyVisibleSchemas);
+ assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+ + " this backend/Android API level combination.");
+ }
+ // @exportToFramework:endStrip()
}
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 9f38b9c..7df269c 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
@@ -751,7 +751,7 @@
snapshotResults("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+ SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4);
@@ -761,7 +761,7 @@
snapshotResults("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
- SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+ SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4, inEmail3);
@@ -772,7 +772,7 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
SearchSpec.GROUPING_TYPE_PER_NAMESPACE
- | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4, inEmail3);
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
index 40d97ef..6e77eae 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
@@ -18,23 +18,18 @@
package androidx.appsearch.cts.app;
import android.content.Context;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.playservicesstorage.PlayServicesStorage;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Assume;
import org.junit.Ignore;
-// TODO(b/237116468): Remove SdkSuppress once AppSearchAttributionSource available for lower API
-// levels.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class GlobalSearchSessionGmsCtsTest extends GlobalSearchSessionCtsTestBase {
private final Context mContext = ApplicationProvider.getApplicationContext();
private boolean mIsGmsAvailable;
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 c2b06d4..a50f7fc 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
@@ -20,14 +20,20 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
import android.content.Context;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.PutDocumentsRequest;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.localstorage.LocalStorage;
import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableSet;
@@ -50,7 +56,99 @@
assertThat(request.getGenericDocuments().get(1).getId()).isEqualTo("test2");
}
-// @exportToFramework:startStrip()
+ @Test
+ public void duplicateIdForNormalAndTakenActionGenericDocumentThrowsException()
+ throws Exception {
+ GenericDocument normalDocument = new GenericDocument.Builder<>(
+ "namespace", "id", "builtin:Thing").build();
+ GenericDocument takenActionGenericDocument = new GenericDocument.Builder<>(
+ "namespace", "id", "builtin:ClickAction").build();
+
+ PutDocumentsRequest.Builder builder = new PutDocumentsRequest.Builder()
+ .addGenericDocuments(normalDocument)
+ .addTakenActionGenericDocuments(takenActionGenericDocument);
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> builder.build());
+ assertThat(e.getMessage()).isEqualTo("Document id " + takenActionGenericDocument.getId()
+ + " cannot exist in both taken action and normal document");
+ }
+
+ @Test
+ public void addTakenActionGenericDocuments() throws Exception {
+ GenericDocument searchActionGenericDocument1 = new GenericDocument.Builder<>(
+ "namespace", "search1", "builtin:SearchAction").build();
+ GenericDocument clickActionGenericDocument1 = new GenericDocument.Builder<>(
+ "namespace", "click1", "builtin:ClickAction").build();
+ GenericDocument clickActionGenericDocument2 = new GenericDocument.Builder<>(
+ "namespace", "click2", "builtin:ClickAction").build();
+ GenericDocument searchActionGenericDocument2 = new GenericDocument.Builder<>(
+ "namespace", "search2", "builtin:SearchAction").build();
+ GenericDocument clickActionGenericDocument3 = new GenericDocument.Builder<>(
+ "namespace", "click3", "builtin:ClickAction").build();
+ GenericDocument clickActionGenericDocument4 = new GenericDocument.Builder<>(
+ "namespace", "click4", "builtin:ClickAction").build();
+ GenericDocument clickActionGenericDocument5 = new GenericDocument.Builder<>(
+ "namespace", "click5", "builtin:ClickAction").build();
+
+ PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+ .addTakenActionGenericDocuments(
+ searchActionGenericDocument1, clickActionGenericDocument1,
+ clickActionGenericDocument2, searchActionGenericDocument2,
+ clickActionGenericDocument3, clickActionGenericDocument4,
+ clickActionGenericDocument5)
+ .build();
+
+ // Generic documents should contain nothing.
+ assertThat(request.getGenericDocuments()).isEmpty();
+
+ // Taken action generic documents should contain correct taken action generic documents.
+ assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+ assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+ assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+ assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+ assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+ assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+ assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+ assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+ }
+
+ @Test
+ public void addTakenActionGenericDocuments_byCollection() throws Exception {
+ Set<GenericDocument> takenActionGenericDocuments = ImmutableSet.of(
+ new GenericDocument.Builder<>(
+ "namespace", "search1", "builtin:SearchAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "click1", "builtin:ClickAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "click2", "builtin:ClickAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "search2", "builtin:SearchAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "click3", "builtin:ClickAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "click4", "builtin:ClickAction").build(),
+ new GenericDocument.Builder<>(
+ "namespace", "click5", "builtin:ClickAction").build());
+
+ PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+ .addTakenActionGenericDocuments(takenActionGenericDocuments)
+ .build();
+
+ // Generic documents should contain nothing.
+ assertThat(request.getGenericDocuments()).isEmpty();
+
+ // Taken action generic documents should contain correct taken action generic documents.
+ assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+ assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+ assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+ assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+ assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+ assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+ assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+ assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+ }
+
+ // @exportToFramework:startStrip()
@Document
static class Card {
@Document.Namespace
@@ -87,5 +185,84 @@
assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("cardId");
}
+
+ @Test
+ public void addTakenActions() throws Exception {
+ SearchAction searchAction1 =
+ new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+ .build();
+ ClickAction clickAction1 =
+ new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+ .build();
+ ClickAction clickAction2 =
+ new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+ .build();
+ SearchAction searchAction2 =
+ new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+ .build();
+ ClickAction clickAction3 =
+ new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+ .build();
+ ClickAction clickAction4 =
+ new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+ .build();
+ ClickAction clickAction5 =
+ new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+ .build();
+
+ PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+ .addTakenActions(searchAction1, clickAction1, clickAction2, searchAction2,
+ clickAction3, clickAction4, clickAction5)
+ .build();
+
+ // Generic documents should contain nothing.
+ assertThat(request.getGenericDocuments()).isEmpty();
+
+ // Taken action generic documents should contain correct taken action generic documents.
+ assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+ assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+ assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+ assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+ assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+ assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+ assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+ assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+ }
+
+ @Test
+ public void addTakenActions_byCollection() throws Exception {
+ Set<TakenAction> takenActions = ImmutableSet.of(
+ new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+ .build(),
+ new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+ .build(),
+ new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+ .build(),
+ new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+ .build(),
+ new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+ .build(),
+ new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+ .build(),
+ new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+ .build());
+
+ PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+ .addTakenActions(takenActions)
+ .build();
+
+ // Generic documents should contain nothing.
+ assertThat(request.getGenericDocuments()).isEmpty();
+
+ // Taken action generic documents should contain correct taken action generic documents.
+ assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+ assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+ assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+ assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+ assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+ assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+ assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+ assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+ }
// @exportToFramework:endStrip()
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
new file mode 100644
index 0000000..6787a56f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class SchemaVisibilityConfigCtsTest {
+
+ @Test
+ public void testBuildVisibilityConfig() {
+ byte[] cert1 = new byte[32];
+ Arrays.fill(cert1, (byte) 1);
+ byte[] cert2 = new byte[32];
+ Arrays.fill(cert2, (byte) 2);
+ SchemaVisibilityConfig schemaVisibilityConfig = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkg1", cert1))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", cert2))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+
+ assertThat(schemaVisibilityConfig.getRequiredPermissions())
+ .containsExactly(ImmutableSet.of(1, 2));
+ assertThat(schemaVisibilityConfig.getAllowedPackages())
+ .containsExactly(new PackageIdentifier("pkg1", cert1));
+ assertThat(schemaVisibilityConfig.getPubliclyVisibleTargetPackage())
+ .isEqualTo(new PackageIdentifier("pkg2", cert2));
+ }
+
+ @Test
+ public void testVisibilityConfigEquals() {
+ // Create two VisibilityConfig instances with the same properties
+ SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+
+ SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+ .addRequiredPermissions(ImmutableSet.of(1, 2))
+ .build();
+
+ // Test equals method
+ assertThat(visibilityConfig1).isEqualTo(visibilityConfig2);
+ assertThat(visibilityConfig2).isEqualTo(visibilityConfig1);
+ }
+
+ @Test
+ public void testVisibilityConfig_rebuild() {
+ String visibleToPackage = "com.example.package";
+ byte[] visibleToPackageCert = new byte[32];
+
+ String publiclyVisibleTarget = "com.example.test";
+ byte[] publiclyVisibleTargetCert = new byte[32];
+
+ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(new PackageIdentifier(visibleToPackage, visibleToPackageCert))
+ .setPubliclyVisibleTargetPackage(new PackageIdentifier(
+ publiclyVisibleTarget, publiclyVisibleTargetCert))
+ .addRequiredPermissions(ImmutableSet.of(1, 2));
+
+ // Create a VisibilityConfig using the Builder
+ SchemaVisibilityConfig original = builder.build();
+
+ SchemaVisibilityConfig rebuild = builder.clearAllowedPackages()
+ .setPubliclyVisibleTargetPackage(null)
+ .clearRequiredPermissions().build();
+
+ // Check if the properties are set correctly
+ assertThat(original.getAllowedPackages()).containsExactly(
+ new PackageIdentifier(visibleToPackage, visibleToPackageCert));
+ assertThat(original.getPubliclyVisibleTargetPackage()).isEqualTo(
+ new PackageIdentifier(publiclyVisibleTarget, publiclyVisibleTargetCert));
+ assertThat(original.getRequiredPermissions()).containsExactly(ImmutableSet.of(1, 2));
+
+ assertThat(rebuild.getAllowedPackages()).isEmpty();
+ assertThat(rebuild.getPubliclyVisibleTargetPackage()).isNull();
+ assertThat(rebuild.getRequiredPermissions()).isEmpty();
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
index 426d01a..382d5272 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -22,11 +22,18 @@
import androidx.appsearch.app.PropertyPath;
import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import androidx.appsearch.testutil.AppSearchEmail;
+import org.junit.Rule;
import org.junit.Test;
public class SearchResultCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Test
public void testBuildSearchResult() {
@@ -172,4 +179,48 @@
SearchResult rebuildJoinedResult2 = rebuild.getJoinedResults().get(1);
assertThat(rebuildJoinedResult2.getGenericDocument().getId()).isEqualTo("id3");
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public void testBuildSearchResult_informationalRankingSignals() {
+ AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+ .setBody("Hello World.")
+ .build();
+ SearchResult searchResult = new SearchResult.Builder("packageName", "databaseName")
+ .setGenericDocument(email)
+ .setRankingSignal(2.9)
+ .addInformationalRankingSignal(3.0)
+ .addInformationalRankingSignal(4.0)
+ .build();
+
+ assertThat(searchResult.getRankingSignal()).isEqualTo(2.9);
+ assertThat(searchResult.getInformationalRankingSignals())
+ .containsExactly(3.0, 4.0).inOrder();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public void testRebuild_informationalRankingSignals() {
+ AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+ .setBody("Hello World.")
+ .build();
+
+ SearchResult.Builder searchResultBuilder =
+ new SearchResult.Builder("packageName", "databaseName")
+ .setGenericDocument(email)
+ .setRankingSignal(2.9)
+ .addInformationalRankingSignal(3.0)
+ .addInformationalRankingSignal(4.0);
+
+ SearchResult original = searchResultBuilder.build();
+ SearchResult rebuild = searchResultBuilder.addInformationalRankingSignal(5).build();
+
+ // Rebuild won't effect the original object
+ assertThat(original.getRankingSignal()).isEqualTo(2.9);
+ assertThat(original.getInformationalRankingSignals()).containsExactly(3.0, 4.0).inOrder();
+
+ assertThat(rebuild.getRankingSignal()).isEqualTo(2.9);
+ assertThat(rebuild.getInformationalRankingSignals())
+ .containsExactly(3.0, 4.0, 5.0).inOrder();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index f465eaf..a6820fb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -24,14 +24,20 @@
import static org.junit.Assert.assertThrows;
import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PropertyPath;
import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import org.junit.Rule;
import org.junit.Test;
import java.util.Collections;
@@ -40,6 +46,9 @@
import java.util.Set;
public class SearchSpecCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testBuildSearchSpecWithoutTermMatch() {
SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("testSchemaType").build();
@@ -118,6 +127,52 @@
}
@Test
+ public void testBuildSearchSpec_searchSourceLogTag() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .setSearchSourceLogTag("logTag")
+ .build();
+
+ assertThat(searchSpec.getSearchSourceLogTag()).isEqualTo("logTag");
+ }
+
+ @Test
+ public void testBuildSearchSpec_searchSourceLogTag_exceedLengthLimitation() {
+ String longTag = new String(new char[110]);
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new SearchSpec.Builder()
+ .setSearchSourceLogTag(longTag));
+ assertThat(e).hasMessageThat().contains(
+ "The maximum supported tag length is 100. This tag is too long");
+ }
+
+ @Test
+ public void testBuildSearchSpec_searchSourceLogTag_defaultIsNull() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .build();
+
+ assertThat(searchSpec.getSearchSourceLogTag()).isNull();
+ }
+
+ // TODO(b/309826655): Flag guard this test.
+ @Test
+ public void testBuildSearchSpec_hasProperty() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .build();
+
+ assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+ assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+ }
+
+ @Test
public void testGetProjectionTypePropertyMasks() {
SearchSpec searchSpec = new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -404,6 +459,24 @@
assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
}
+ // TODO(b/309826655): Flag guard this test.
+ @Test
+ public void testSetFeatureEnabledToFalse_hasProperty() {
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec = builder
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+
+ searchSpec = builder
+ .setListFilterQueryLanguageEnabled(false)
+ .setListFilterHasPropertyFunctionEnabled(false)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+ assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isFalse();
+ }
@Test
public void testInvalidAdvancedRanking() {
@@ -463,20 +536,6 @@
}
@Test
- public void testProjections_withSchemaFilter() throws Exception {
- SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
- .addFilterSchemas("Filter")
- .addProjectionPathsForDocumentClass(King.class, ImmutableList.of(
- new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
-
- IllegalArgumentException exception =
- assertThrows(IllegalArgumentException.class, searchSpecBuilder::build);
- assertThat(exception.getMessage())
- .isEqualTo("Projection requested for schema not in schemas filters: King");
- }
-
- @Test
public void testTypePropertyWeightsForDocumentClass() throws Exception {
SearchSpec searchSpec = new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -553,6 +612,40 @@
}
@Test
+ public void testGetPropertyFiltersTypePropertyMasks() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+ .addFilterProperties("TypeB", ImmutableList.of("field7"))
+ .addFilterProperties("TypeC", ImmutableList.of())
+ .build();
+
+ Map<String, List<String>> typePropertyPathMap = searchSpec.getFilterProperties();
+ assertThat(typePropertyPathMap.keySet())
+ .containsExactly("TypeA", "TypeB", "TypeC");
+ assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
+ assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
+ assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+ }
+
+ @Test
+ public void testFilterSchemas_wildcardProjection() {
+ // Should not crash
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("ParentType")
+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.singletonList("TypeA"))
+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+ Collections.singletonList("TypeB"))
+ .build();
+
+ assertThat(searchSpec.getFilterSchemas()).containsExactly("ParentType");
+ assertThat(searchSpec.getProjections())
+ .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeA"));
+ assertThat(searchSpec.getFilterProperties())
+ .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeB"));
+ }
+
+ @Test
public void testRebuild() {
JoinSpec originalJoinSpec = new JoinSpec.Builder("entityId")
.setNestedSearch("joe", new SearchSpec.Builder().addFilterSchemas("Action").build())
@@ -583,4 +676,163 @@
assertThat(rebuild.getJoinSpec().getNestedSearchSpec().getFilterSchemas())
.containsExactly("CallAction");
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testEmbeddingSearch() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(embedding1, embedding2)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+ assertThat(searchSpec.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+ assertThat(searchSpec.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testRebuild_embeddingSearch() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+ // Create a builder
+ SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .setDefaultEmbeddingSearchMetricType(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+ .addSearchEmbeddings(embedding1);
+ SearchSpec searchSpec1 = searchSpecBuilder.build();
+
+ // Add a new embedding to the builder and rebuild. We should see that the new embedding
+ // is only added to searchSpec2.
+ searchSpecBuilder.addSearchEmbeddings(embedding2);
+ SearchSpec searchSpec2 = searchSpecBuilder.build();
+
+ assertThat(searchSpec1.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec1.isEmbeddingSearchEnabled()).isTrue();
+ assertThat(searchSpec1.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+ assertThat(searchSpec1.getSearchEmbeddings()).containsExactly(embedding1);
+
+ assertThat(searchSpec2.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec2.isEmbeddingSearchEnabled()).isTrue();
+ assertThat(searchSpec2.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+ SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+ assertThat(searchSpec2.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testBuildSearchSpec_embeddingSearch() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterHasPropertyFunctionEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+
+ assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+ assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+ assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public void testSetFeatureEnabledToFalse_embeddingSearch() {
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec = builder
+ .setListFilterQueryLanguageEnabled(true)
+ .setEmbeddingSearchEnabled(true)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+
+ searchSpec = builder
+ .setListFilterQueryLanguageEnabled(false)
+ .setEmbeddingSearchEnabled(false)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+ assertThat(searchSpec.isEmbeddingSearchEnabled()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public void testListFilterTokenizeFunction() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterTokenizeFunctionEnabled(true)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public void testSetFeatureEnabledToFalse_tokenizeFunction() {
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec = builder
+ .setListFilterQueryLanguageEnabled(true)
+ .setListFilterTokenizeFunctionEnabled(true)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+
+ searchSpec = builder
+ .setListFilterTokenizeFunctionEnabled(false)
+ .build();
+ assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+ assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public void testInformationalRankingExpressions() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .setRankingStrategy("this.documentScore()")
+ .addInformationalRankingExpressions("this.relevanceScore()")
+ .build();
+ assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+ assertThat(searchSpec.getRankingStrategy())
+ .isEqualTo(SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION);
+ assertThat(searchSpec.getAdvancedRankingExpression())
+ .isEqualTo("this.documentScore()");
+ assertThat(searchSpec.getInformationalRankingExpressions()).containsExactly(
+ "this.relevanceScore()");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public void testRebuild_informationalRankingExpressions() {
+ SearchSpec.Builder searchSpecBuilder =
+ new SearchSpec.Builder().addInformationalRankingExpressions(
+ "this.relevanceScore()");
+
+ SearchSpec original = searchSpecBuilder.build();
+ SearchSpec rebuild = searchSpecBuilder
+ .addInformationalRankingExpressions("this.documentScore()")
+ .build();
+
+ // Rebuild won't effect the original object
+ assertThat(original.getInformationalRankingExpressions())
+ .containsExactly("this.relevanceScore()");
+
+ assertThat(rebuild.getInformationalRankingExpressions())
+ .containsExactly("this.relevanceScore()", "this.documentScore()").inOrder();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
index 69979c3..6f1e79b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertThrows;
+import androidx.appsearch.app.PropertyPath;
import androidx.appsearch.app.SearchSuggestionSpec;
import com.google.common.collect.ImmutableList;
@@ -75,6 +76,38 @@
}
@Test
+ public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
+ SearchSuggestionSpec searchSuggestionSpec =
+ new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+ .setRankingStrategy(SearchSuggestionSpec
+ .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
+ .addFilterSchemas("Person", "Email")
+ .addFilterSchemas(ImmutableList.of("Foo"))
+ .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+ .addFilterPropertyPaths("Foo",
+ ImmutableList.of(new PropertyPath("Bar")))
+ .build();
+
+ assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
+ assertThat(searchSuggestionSpec.getFilterSchemas())
+ .containsExactly("Person", "Email", "Foo");
+ assertThat(searchSuggestionSpec.getFilterProperties())
+ .containsExactly("Email", ImmutableList.of("Subject", "body"),
+ "Foo", ImmutableList.of("Bar"));
+ }
+
+ @Test
+ public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
+ IllegalStateException e = assertThrows(IllegalStateException.class,
+ () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+ .addFilterSchemas("Person")
+ .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+ .build());
+ assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
+ + "property filter but doesn't exist in the schema filter.");
+ }
+
+ @Test
public void testRebuild() throws Exception {
SearchSuggestionSpec.Builder builder =
new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
@@ -106,4 +139,31 @@
assertThat(rebuild.getFilterSchemas())
.containsExactly("Person", "Email", "Message", "Foo");
}
+
+ @Test
+ public void testRebuild_withPropertyFilter() throws Exception {
+ SearchSuggestionSpec.Builder builder =
+ new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+ .addFilterSchemas("Person", "Email")
+ .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
+
+ SearchSuggestionSpec original = builder.build();
+
+ builder.addFilterSchemas("Message", "Foo")
+ .addFilterProperties("Foo", ImmutableList.of("Bar"));
+ SearchSuggestionSpec rebuild = builder.build();
+
+ assertThat(original.getMaximumResultCount()).isEqualTo(123);
+ assertThat(original.getFilterSchemas())
+ .containsExactly("Person", "Email");
+ assertThat(original.getFilterProperties())
+ .containsExactly("Email", ImmutableList.of("Subject", "body"));
+
+ assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
+ assertThat(rebuild.getFilterSchemas())
+ .containsExactly("Person", "Email", "Message", "Foo");
+ assertThat(rebuild.getFilterProperties())
+ .containsExactly("Email", ImmutableList.of("Subject", "body"),
+ "Foo", ImmutableList.of("Bar"));
+ }
}
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 d7de450..1876b87 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
@@ -30,6 +30,7 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.testutil.AppSearchEmail;
@@ -201,6 +202,32 @@
}
@Test
+ public void testInvalidSchemaReferences_fromPubliclyVisible() {
+ IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+ () -> new SetSchemaRequest.Builder().setPubliclyVisibleSchema("InvalidSchema",
+ new PackageIdentifier("com.foo.package",
+ /*sha256Certificate=*/ new byte[]{})).build());
+ assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+ }
+
+ @Test
+ public void testInvalidSchemaReferences_fromVisibleToConfigs() {
+ byte[] sha256cert1 = new byte[32];
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+
+ IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+ () -> new SetSchemaRequest.Builder()
+ .addSchemaTypeVisibleToConfig("InvalidSchema", config)
+ .build());
+ assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+ }
+
+ @Test
public void testSetSchemaTypeDisplayedBySystem_displayed() {
AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
@@ -276,7 +303,7 @@
"Schema2", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
// Clear the permissions in the builder
setSchemaRequestBuilder.clearRequiredPermissionsForSchemaTypeVisibility("Schema1");
@@ -287,7 +314,7 @@
"Schema2", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
// Old object should remain unchanged
assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -300,7 +327,7 @@
"Schema2", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
}
@Test
@@ -375,6 +402,132 @@
assertThat(request.getSchemasVisibleToPackages()).isEmpty();
}
+ @Test
+ public void testPubliclyVisibleSchemaType() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier =
+ new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+ "Schema", packageIdentifier).build();
+ assertThat(request.getPubliclyVisibleSchemas())
+ .containsExactly("Schema", packageIdentifier);
+ }
+
+ @Test
+ public void testPubliclyVisibleSchemaType_removal() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier =
+ new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+ "Schema", packageIdentifier).build();
+ assertThat(request.getPubliclyVisibleSchemas())
+ .containsExactly("Schema", packageIdentifier);
+
+ // Removed Schema
+ request = new SetSchemaRequest.Builder().addSchemas(schema)
+ .setPubliclyVisibleSchema("Schema", packageIdentifier)
+ .setPubliclyVisibleSchema("Schema", null)
+ .build();
+ assertThat(request.getPubliclyVisibleSchemas()).isEmpty();
+ }
+
+ @Test
+ public void testPubliclyVisibleSchemaType_deduped() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier =
+ new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+ PackageIdentifier packageIdentifier2 =
+ new PackageIdentifier("com.package.bar", /*sha256Certificate=*/ new byte[]{});
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+ "Schema", packageIdentifier).build();
+ assertThat(request.getPubliclyVisibleSchemas())
+ .containsExactly("Schema", packageIdentifier);
+
+ // Deduped schema
+ request = new SetSchemaRequest.Builder().addSchemas(schema)
+ .setPubliclyVisibleSchema("Schema", packageIdentifier2)
+ .setPubliclyVisibleSchema("Schema", packageIdentifier)
+ .build();
+ assertThat(request.getPubliclyVisibleSchemas())
+ .containsExactly("Schema", packageIdentifier);
+ }
+
+ @Test
+ public void testSetSchemaTypeVisibleForConfigs() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+ new byte[]{100});
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier2)
+ .addRequiredPermissions(ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema)
+ .addSchemaTypeVisibleToConfig("Schema", config1)
+ .addSchemaTypeVisibleToConfig("Schema", config2)
+ .build();
+
+ assertThat(request.getSchemasVisibleToConfigs()).containsExactly("Schema",
+ ImmutableSet.of(config1, config2));
+ }
+
+ @Test
+ public void testClearSchemaTypeVisibleForConfigs() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+ new byte[]{100});
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier2)
+ .addRequiredPermissions(ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+ .addSchemas(schema)
+ .addSchemaTypeVisibleToConfig("Schema", config1)
+ .addSchemaTypeVisibleToConfig("Schema", config2);
+
+ SetSchemaRequest original = builder.build();
+ assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+ ImmutableSet.of(config1, config2));
+
+ builder.clearSchemaTypeVisibleToConfigs("Schema");
+ SetSchemaRequest rebuild = builder.build();
+
+ // rebuild has empty visible to configs
+ assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+ // original keep in the same state
+ assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+ ImmutableSet.of(config1, config2));
+ }
// @exportToFramework:startStrip()
@Document
@@ -611,7 +764,7 @@
"Queen", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
// Clear the permissions in the builder
setSchemaRequestBuilder.clearRequiredPermissionsForDocumentClassVisibility(King.class);
@@ -622,7 +775,7 @@
"Queen", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
// Old object should remain unchanged
assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -635,7 +788,76 @@
"Queen", ImmutableSet.of(
ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
)
- );
+ );
+ }
+
+ @Test
+ public void testSetDocumentClassVisibleForConfigs() throws Exception {
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+ new byte[]{100});
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier2)
+ .addRequiredPermissions(ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addDocumentClasses(King.class, Queen.class)
+ .addDocumentClassVisibleToConfig(King.class, config1)
+ .addDocumentClassVisibleToConfig(King.class, config2)
+ .build();
+
+ assertThat(request.getSchemasVisibleToConfigs()).containsExactly("King",
+ ImmutableSet.of(config1, config2));
+ }
+
+ @Test
+ public void testClearDocumentClassVisibleForConfigs() throws Exception {
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+ new byte[]{100});
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier2)
+ .addRequiredPermissions(ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+ .addDocumentClasses(King.class, Queen.class)
+ .addDocumentClassVisibleToConfig(King.class, config1)
+ .addDocumentClassVisibleToConfig(King.class, config2);
+
+ SetSchemaRequest original = builder.build();
+
+ assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+ ImmutableSet.of(config1, config2));
+
+ // Clear the visbleToConfigs
+ builder.clearDocumentClassVisibleToConfigs(King.class);
+ SetSchemaRequest rebuild = builder.build();
+
+ // rebuild object has empty visibleToConfigs
+ assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+ // original keep in same state.
+ assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+ ImmutableSet.of(config1, config2));
}
// @exportToFramework:endStrip()
@@ -740,6 +962,91 @@
}
@Test
+ public void testRebuild_visibleConfigs() {
+ 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);
+
+ SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier1)
+ .addRequiredPermissions(
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ .build();
+ SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+ .addAllowedPackage(packageIdentifier2)
+ .addRequiredPermissions(ImmutableSet.of(
+ SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+ SetSchemaRequest.READ_CALENDAR))
+ .build();
+
+ 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)
+ .addSchemaTypeVisibleToConfig("Email1", config1);
+
+ SetSchemaRequest original = builder.build();
+ SetSchemaRequest rebuild = builder
+ .addSchemas(schema2)
+ .addSchemaTypeVisibleToConfig("Email2", config2)
+ .build();
+
+ assertThat(original.getSchemas()).containsExactly(schema1);
+ assertThat(original.getSchemasVisibleToConfigs()).containsExactly(
+ "Email1", ImmutableSet.of(config1));
+
+ assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+ assertThat(rebuild.getSchemasVisibleToConfigs()).containsExactly(
+ "Email1", ImmutableSet.of(config1),
+ "Email2", ImmutableSet.of(config2));
+ }
+
+ @Test
+ public void testSetVisibility_publicVisibility_rebuild() {
+ 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").build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2").build();
+
+ SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+ .addSchemas(schema1).setPubliclyVisibleSchema("Email1", packageIdentifier1);
+
+ SetSchemaRequest original = builder.build();
+ SetSchemaRequest rebuild = builder.addSchemas(schema2)
+ .setPubliclyVisibleSchema("Email2", packageIdentifier2).build();
+
+ assertThat(original.getSchemas()).containsExactly(schema1);
+ assertThat(original.getPubliclyVisibleSchemas())
+ .containsExactly("Email1", packageIdentifier1);
+
+ assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+ assertThat(original.getPubliclyVisibleSchemas())
+ .containsExactly("Email1", packageIdentifier1);
+ }
+
+ @Test
public void getAndModify() {
byte[] sha256cert1 = new byte[32];
byte[] sha256cert2 = new byte[32];
@@ -769,7 +1076,8 @@
.build();
// get the visibility setting and modify the output object.
- // skip getSchemasNotDisplayedBySystem since it returns an unmodifiable object.
+ // skip getSchemasNotDisplayedBySystem and getPubliclyVisibleSchemas since they return
+ // unmodifiable objects.
request.getSchemasVisibleToPackages().put("Email2", ImmutableSet.of(packageIdentifier2));
request.getRequiredPermissionsForSchemaTypeVisibility().put("Email2",
ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CALENDAR)));
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
index 06f1bd4..55840c6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
@@ -27,17 +27,17 @@
public class DocumentChangeInfoCtsTest {
@Test
public void testConstructor() {
- DocumentChangeInfo DocumentChangeInfo = new DocumentChangeInfo(
+ 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())
+ 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");
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
new file mode 100644
index 0000000..48f7846
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class ClickActionCtsTest {
+ @Test
+ public void testBuilder() {
+ ClickAction clickAction =
+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setReferencedQualifiedId("pkg$db/ns#refId")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(3)
+ .setTimeStayOnResultMillis(65536)
+ .build();
+
+ assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+ assertThat(clickAction.getId()).isEqualTo("id");
+ assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(clickAction.getDocumentTtlMillis()).isEqualTo(456);
+ assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickAction.getQuery()).isEqualTo("query");
+ assertThat(clickAction.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+ assertThat(clickAction.getResultRankInBlock()).isEqualTo(1);
+ assertThat(clickAction.getResultRankGlobal()).isEqualTo(3);
+ assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(65536);
+ }
+
+ @Test
+ public void testBuilder_defaultValues() {
+ ClickAction clickAction =
+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .build();
+
+ assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+ assertThat(clickAction.getId()).isEqualTo("id");
+ assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(clickAction.getDocumentTtlMillis())
+ .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+ assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickAction.getQuery()).isNull();
+ assertThat(clickAction.getReferencedQualifiedId()).isNull();
+ assertThat(clickAction.getResultRankInBlock()).isEqualTo(-1);
+ assertThat(clickAction.getResultRankGlobal()).isEqualTo(-1);
+ assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testBuilderCopy_allFieldsAreCopied() {
+ ClickAction clickAction1 =
+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setReferencedQualifiedId("pkg$db/ns#refId")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(3)
+ .setTimeStayOnResultMillis(65536)
+ .build();
+ ClickAction clickAction2 = new ClickAction.Builder(clickAction1).build();
+
+ // All fields should be copied correctly from clickAction1 to the builder and propagates to
+ // clickAction2 after calling build().
+ assertThat(clickAction2.getNamespace()).isEqualTo("namespace");
+ assertThat(clickAction2.getId()).isEqualTo("id");
+ assertThat(clickAction2.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(clickAction2.getDocumentTtlMillis()).isEqualTo(456);
+ assertThat(clickAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(clickAction2.getQuery()).isEqualTo("query");
+ assertThat(clickAction2.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+ assertThat(clickAction2.getResultRankInBlock()).isEqualTo(1);
+ assertThat(clickAction2.getResultRankGlobal()).isEqualTo(3);
+ assertThat(clickAction2.getTimeStayOnResultMillis()).isEqualTo(65536);
+ }
+
+ @Test
+ public void testToGenericDocument() throws Exception {
+ ClickAction clickAction =
+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setReferencedQualifiedId("pkg$db/ns#refId")
+ .setResultRankInBlock(1)
+ .setResultRankGlobal(3)
+ .setTimeStayOnResultMillis(65536)
+ .build();
+
+ GenericDocument document = GenericDocument.fromDocumentClass(clickAction);
+ assertThat(document.getNamespace()).isEqualTo("namespace");
+ assertThat(document.getId()).isEqualTo("id");
+ assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+ assertThat(document.getTtlMillis()).isEqualTo(456);
+ assertThat(document.getPropertyLong("actionType"))
+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+ assertThat(document.getPropertyString("query")).isEqualTo("query");
+ assertThat(document.getPropertyString("referencedQualifiedId"))
+ .isEqualTo("pkg$db/ns#refId");
+ assertThat(document.getPropertyLong("resultRankInBlock")).isEqualTo(1);
+ assertThat(document.getPropertyLong("resultRankGlobal")).isEqualTo(3);
+ assertThat(document.getPropertyLong("timeStayOnResultMillis")).isEqualTo(65536);
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
new file mode 100644
index 0000000..a4a63a6
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class SearchActionCtsTest {
+ @Test
+ public void testBuilder() {
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setFetchedResultCount(1)
+ .build();
+
+ assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+ assertThat(searchAction.getId()).isEqualTo("id");
+ assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(searchAction.getDocumentTtlMillis()).isEqualTo(456);
+ assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchAction.getQuery()).isEqualTo("query");
+ assertThat(searchAction.getFetchedResultCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testBuilder_defaultValues() {
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .build();
+
+ assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+ assertThat(searchAction.getId()).isEqualTo("id");
+ assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(searchAction.getDocumentTtlMillis())
+ .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+ assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchAction.getQuery()).isNull();
+ assertThat(searchAction.getFetchedResultCount()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testBuilderCopy_allFieldsAreCopied() {
+ SearchAction searchAction1 =
+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setFetchedResultCount(1)
+ .build();
+ SearchAction searchAction2 = new SearchAction.Builder(searchAction1).build();
+
+ assertThat(searchAction2.getNamespace()).isEqualTo("namespace");
+ assertThat(searchAction2.getId()).isEqualTo("id");
+ assertThat(searchAction2.getActionTimestampMillis()).isEqualTo(123);
+ assertThat(searchAction2.getDocumentTtlMillis()).isEqualTo(456);
+ assertThat(searchAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(searchAction2.getQuery()).isEqualTo("query");
+ assertThat(searchAction2.getFetchedResultCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testToGenericDocument() throws Exception {
+ SearchAction searchAction =
+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+ .setDocumentTtlMillis(456)
+ .setQuery("query")
+ .setFetchedResultCount(1)
+ .build();
+
+ GenericDocument document = GenericDocument.fromDocumentClass(searchAction);
+ assertThat(document.getNamespace()).isEqualTo("namespace");
+ assertThat(document.getId()).isEqualTo("id");
+ assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+ assertThat(document.getTtlMillis()).isEqualTo(456);
+ assertThat(document.getPropertyLong("actionType"))
+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+ assertThat(document.getPropertyString("query")).isEqualTo("query");
+ assertThat(document.getPropertyLong("fetchedResultCount")).isEqualTo(1);
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
new file mode 100644
index 0000000..3ec5386
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Shim for real CheckFlagsRule defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */
+public final class CheckFlagsRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return base;
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
new file mode 100644
index 0000000..1a2858a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+/**
+ * Shim for real DeviceFlagsValueProvider defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */
+public final class DeviceFlagsValueProvider {
+ private DeviceFlagsValueProvider() {}
+
+ /** Provides a shim rule that can be used to check the status of flags on device */
+ public static CheckFlagsRule createCheckFlagsRule() {
+ return new CheckFlagsRule();
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
new file mode 100644
index 0000000..14169d7
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class FlagsTest {
+ @Test
+ public void testFlagValue_enableSafeParcelable2() {
+ assertThat(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2).isEqualTo(
+ "com.android.appsearch.flags.enable_safe_parcelable_2");
+ }
+
+ @Test
+ public void testFlagValue_enableListFilterHasPropertyFunction() {
+ assertThat(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION).isEqualTo(
+ "com.android.appsearch.flags.enable_list_filter_has_property_function");
+ }
+
+ @Test
+ public void testFlagValue_enableGroupingTypePerSchema() {
+ assertThat(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA).isEqualTo(
+ "com.android.appsearch.flags.enable_grouping_type_per_schema");
+ }
+
+ @Test
+ public void testFlagValue_enableGenericDocumentCopyConstructor() {
+ assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR).isEqualTo("com.android"
+ + ".appsearch.flags.enable_generic_document_copy_constructor");
+ }
+
+ @Test
+ public void testFlagValue_enableSearchSpecFilterProperties() {
+ assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES).isEqualTo(
+ "com.android.appsearch.flags.enable_search_spec_filter_properties");
+ }
+
+ @Test
+ public void testFlagValue_enableSearchSpecSetSearchSourceLogTag() {
+ assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG).isEqualTo(
+ "com.android.appsearch.flags.enable_search_spec_set_search_source_log_tag");
+ }
+
+ @Test
+ public void testFlagValue_enableSetSchemaVisibleToConfigs() {
+ assertThat(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS).isEqualTo("com"
+ + ".android.appsearch.flags.enable_set_schema_visible_to_configs");
+ }
+
+ @Test
+ public void testFlagValue_enablePutDocumentsRequestAddTakenActions() {
+ assertThat(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS).isEqualTo(
+ "com.android.appsearch.flags.enable_put_documents_request_add_taken_actions");
+ }
+
+ @Test
+ public void testFlagValue_enableGenericDocumentBuilderHiddenMethods() {
+ assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS).isEqualTo("com"
+ + ".android.appsearch.flags.enable_generic_document_builder_hidden_methods");
+ }
+
+ @Test
+ public void testFlagValue_enableSetPubliclyVisibleSchema() {
+ assertThat(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ .isEqualTo(
+ "com.android.appsearch.flags.enable_set_publicly_visible_schema");
+ }
+
+ @Test
+ public void testFlagValue_enableEnterpriseGlobalSearchSession() {
+ assertThat(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
+ .isEqualTo("com.android.appsearch.flags.enable_enterprise_global_search_session");
+ }
+
+ @Test
+ public void testFlagValue_enableResultDeniedAndResultRateLimited() {
+ assertThat(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
+ .isEqualTo(
+ "com.android.appsearch.flags.enable_result_denied_and_result_rate_limited");
+ }
+
+ @Test
+ public void testFlagValue_enableGetParentTypesAndIndexableNestedProperties() {
+ assertThat(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
+ .isEqualTo(
+ "com.android.appsearch.flags"
+ + ".enable_get_parent_types_and_indexable_nested_properties");
+ }
+
+ @Test
+ public void testFlagValue_enableSchemaEmbeddingPropertyConfig() {
+ assertThat(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ .isEqualTo("com.android.appsearch.flags.enable_schema_embedding_property_config");
+ }
+
+ @Test
+ public void testFlagValue_enableListFilterTokenizeFunction() {
+ assertThat(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ .isEqualTo("com.android.appsearch.flags.enable_list_filter_tokenize_function");
+ }
+
+ @Test
+ public void testFlagValue_enableInformationalRankingExpressions() {
+ assertThat(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ .isEqualTo("com.android.appsearch.flags.enable_informational_ranking_expressions");
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
new file mode 100644
index 0000000..d4f3bf0
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Shim for real RequiresFlagsEnabled defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ *
+ * <p>In Framework, indicates that a specific test or class should be run only if all of the given
+ * feature flags are enabled in the device's current state. Enforced by the {@code CheckFlagsRule}.
+ *
+ * <p>This annotation works together with RequiresFlagsDisabled to define the value that is
+ * required of the flag by the test for the test to run. It is an error for either a method or class
+ * to require that a particular flag be both enabled and disabled.
+ *
+ * <p>If the value of a particular flag is required (by either {@code RequiresFlagsEnabled} or
+ * {@code RequiresFlagsDisabled}) by both the class and test method, then the values must be
+ * consistent.
+ *
+ * <p>If the value of a one flag is required by an annotation on the class, and the value of a
+ * different flag is required by an annotation of the method, then both requirements apply.
+ *
+ * <p>With {@code CheckFlagsRule}, test(s) will be skipped with 'assumption failed' when any of the
+ * required flag on the target Android platform is disabled.
+ *
+ * <p>Both {@code SetFlagsRule} and {@code CheckFlagsRule} will fail the test if a particular flag
+ * is both set (with {@code EnableFlags} or {@code DisableFlags}) and required (with {@code
+ * RequiresFlagsEnabled} or {@code RequiresFlagsDisabled}).
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresFlagsEnabled {
+ /**
+ * The list of the feature flags that require to be enabled. Each item is the full flag name
+ * with the format {package_name}.{flag_name}.
+ */
+ String[] value();
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
index d8cc9bc..0900ec8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
@@ -20,9 +20,15 @@
import static org.junit.Assert.assertThrows;
+import android.os.Parcel;
+
+import androidx.appsearch.app.EmbeddingVector;
+
import org.junit.Test;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
/** Tests for {@link androidx.appsearch.app.GenericDocument} related SafeParcels. */
@@ -34,6 +40,8 @@
double[] doubleValues = {1.0, 2.0};
boolean[] booleanValues = {true, false};
byte[][] bytesValues = {new byte[1]};
+ EmbeddingVector[] embeddingValues = {new EmbeddingVector(new float[1],
+ "my_model")};
GenericDocumentParcel[] docValues = {(new GenericDocumentParcel.Builder(
"namespace", "id", "schemaType")).build()};
@@ -52,6 +60,9 @@
assertThat(new PropertyParcel.Builder("name").setBytesValues(
bytesValues).build().getBytesValues()).isEqualTo(
Arrays.copyOf(bytesValues, bytesValues.length));
+ assertThat(new PropertyParcel.Builder("name").setEmbeddingValues(
+ embeddingValues).build().getEmbeddingValues()).isEqualTo(
+ Arrays.copyOf(embeddingValues, embeddingValues.length));
assertThat(new PropertyParcel.Builder("name").setDocumentValues(
docValues).build().getDocumentValues()).isEqualTo(
Arrays.copyOf(docValues, docValues.length));
@@ -85,14 +96,14 @@
builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
GenericDocumentParcel genericDocumentParcel = builder.build();
- PropertyParcel[] properties = genericDocumentParcel.getProperties();
+ List<PropertyParcel> properties = genericDocumentParcel.getProperties();
Map<String, PropertyParcel> propertyMap = genericDocumentParcel.getPropertyMap();
PropertyParcel longArrayProperty = new PropertyParcel.Builder(
/*name=*/ "longArray").setLongValues(longArray).build();
PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
/*name=*/ "stringArray").setStringValues(stringArray).build();
- assertThat(properties).asList().containsExactly(longArrayProperty, stringArrayProperty);
+ assertThat(properties).containsExactly(longArrayProperty, stringArrayProperty);
assertThat(propertyMap).containsExactly("longArray", longArrayProperty,
"stringArray", stringArrayProperty);
}
@@ -106,8 +117,10 @@
/*schemaType=*/ "schemaType");
long[] longArray = new long[]{1L, 2L, 3L};
String[] stringArray = new String[]{"hello", "world", "!"};
+ List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+ builder.setParentTypes(parentTypes);
GenericDocumentParcel genericDocumentParcel = builder.build();
GenericDocumentParcel genericDocumentParcelCopy =
@@ -124,9 +137,130 @@
genericDocumentParcel.getTtlMillis());
assertThat(genericDocumentParcelCopy.getScore()).isEqualTo(
genericDocumentParcel.getScore());
+ assertThat(genericDocumentParcelCopy.getParentTypes()).isEqualTo(
+ genericDocumentParcel.getParentTypes());
+
// Check it is a copy.
assertThat(genericDocumentParcelCopy).isNotSameInstanceAs(genericDocumentParcel);
assertThat(genericDocumentParcelCopy.getProperties()).isEqualTo(
genericDocumentParcel.getProperties());
}
+
+ @Test
+ public void testGenericDocumentParcelWithParentTypes() {
+ GenericDocumentParcel.Builder builder =
+ new GenericDocumentParcel.Builder(
+ /*namespace=*/ "namespace",
+ /*id=*/ "id",
+ /*schemaType=*/ "schemaType");
+ List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+
+ builder.setParentTypes(parentTypes);
+ GenericDocumentParcel genericDocumentParcel = builder.build();
+
+ assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+ }
+
+ @Test
+ public void testGenericDocumentParcel_builderCanBeReused() {
+ GenericDocumentParcel.Builder builder =
+ new GenericDocumentParcel.Builder(
+ /*namespace=*/ "namespace",
+ /*id=*/ "id",
+ /*schemaType=*/ "schemaType");
+ long[] longArray = new long[]{1L, 2L, 3L};
+ String[] stringArray = new String[]{"hello", "world", "!"};
+ List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+ builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+ builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+ builder.setParentTypes(parentTypes);
+
+ GenericDocumentParcel genericDocumentParcel = builder.build();
+ builder.setParentTypes(new ArrayList<>(Arrays.asList("parentType3", "parentType4")));
+ builder.clearProperty("longArray");
+ builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ new String[]{""});
+
+ PropertyParcel longArrayProperty = new PropertyParcel.Builder(
+ /*name=*/ "longArray").setLongValues(longArray).build();
+ PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
+ /*name=*/ "stringArray").setStringValues(stringArray).build();
+ assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+ assertThat(genericDocumentParcel.getPropertyMap()).containsExactly("longArray",
+ longArrayProperty, "stringArray", stringArrayProperty);
+ }
+
+ @Test
+ public void testRecreateFromParcelWithParentTypes() {
+ String[] stringArray = new String[]{"Hello", "world"};
+ long[] longArray = new long[]{1L, 2L};
+ double[] doubleArray = new double[]{1.1, 2.2};
+ boolean[] booleanArray = new boolean[]{true, false};
+ byte[][] bytesArray = new byte[][]{{1, 2}};
+ GenericDocumentParcel inDoc = new GenericDocumentParcel.Builder(
+ "namespace1", "id1", "schema1")
+ .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
+ .setScore(42)
+ .setTtlMillis(43)
+ .setCreationTimestampMillis(44)
+ .putInPropertyMap("propStrings", stringArray)
+ .putInPropertyMap("propLongs", longArray)
+ .putInPropertyMap("propDoubles", doubleArray)
+ .putInPropertyMap("propBytes", bytesArray)
+ .putInPropertyMap("propBooleans", booleanArray)
+ .putInPropertyMap(
+ "propDoc",
+ new GenericDocumentParcel[]{
+ new GenericDocumentParcel.Builder(
+ "namespace2", "id2", "schema2")
+ .putInPropertyMap("propStrings", new String[]{"Goodbye"})
+ .putInPropertyMap("propBytes", new byte[][]{{3, 4}})
+ .build()})
+ .build();
+
+ // Serialize the document
+ Parcel inParcel = Parcel.obtain();
+ inParcel.writeParcelable(inDoc, /*flags=*/ 0);
+ byte[] data = inParcel.marshall();
+ inParcel.recycle();
+
+ // Deserialize the document
+ Parcel outParcel = Parcel.obtain();
+ outParcel.unmarshall(data, 0, data.length);
+ outParcel.setDataPosition(0);
+ @SuppressWarnings("deprecation")
+ GenericDocumentParcel outDoc = outParcel.readParcelable(getClass().getClassLoader());
+ outParcel.recycle();
+
+ // Compare results
+ assertThat(outDoc.getId()).isEqualTo("id1");
+ assertThat(outDoc.getNamespace()).isEqualTo("namespace1");
+ assertThat(outDoc.getSchemaType()).isEqualTo("schema1");
+ assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
+ assertThat(outDoc.getScore()).isEqualTo(42);
+ assertThat(outDoc.getTtlMillis()).isEqualTo(43);
+ assertThat(outDoc.getCreationTimestampMillis()).isEqualTo(44);
+
+ // Properties
+ Map<String, PropertyParcel> propertyMap = outDoc.getPropertyMap();
+ assertThat(propertyMap.get("propStrings").getStringValues()).isEqualTo(stringArray);
+ assertThat(propertyMap.get("propLongs").getLongValues()).isEqualTo(longArray);
+ assertThat(propertyMap.get("propDoubles").getDoubleValues()).isEqualTo(doubleArray);
+ assertThat(propertyMap.get("propBytes").getBytesValues()).isEqualTo(bytesArray);
+ assertThat(propertyMap.get("propBooleans").getBooleanValues()).isEqualTo(booleanArray);
+
+ // Check inner doc.
+ GenericDocumentParcel[] innerDocs = propertyMap.get("propDoc").getDocumentValues();
+ assertThat(innerDocs).hasLength(1);
+ assertThat(innerDocs[0].getNamespace()).isEqualTo("namespace2");
+ assertThat(innerDocs[0].getId()).isEqualTo("id2");
+ assertThat(innerDocs[0].getSchemaType()).isEqualTo("schema2");
+ assertThat(innerDocs[0].getPropertyMap().get("propStrings").getStringValues()).isEqualTo(
+ new String[]{"Goodbye"});
+ assertThat(innerDocs[0].getPropertyMap().get("propBytes").getBytesValues()).isEqualTo(
+ new byte[][]{{3, 4}});
+
+ // Finally check equals and hashcode
+ assertThat(inDoc).isEqualTo(outDoc);
+ assertThat(inDoc.hashCode()).isEqualTo(outDoc.hashCode());
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
new file mode 100644
index 0000000..a5ce50b
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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.safeparcel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class PropertyParcelTest {
+ @Test
+ public void testTwoDimensionByteArray_serializationSupported() {
+ int row = 20;
+ int col = 10;
+ byte[][] bytesArray = new byte[row][col];
+ for (int i = 0; i < row; ++i) {
+ for (int j = 0; j < col; ++j) {
+ bytesArray[i][j] = (byte) (i + j);
+ }
+ }
+
+ String propertyName = "propertyName";
+ PropertyParcel expectedPropertyParcel =
+ new PropertyParcel.Builder(propertyName).setBytesValues(bytesArray).build();
+ Parcel data = Parcel.obtain();
+ try {
+ data.writeParcelable(expectedPropertyParcel, /* flags= */ 0);
+ data.setDataPosition(0);
+ @SuppressWarnings("deprecation")
+ PropertyParcel actualPropertyParcel = data.readParcelable(
+ PropertyParcelTest.class.getClassLoader());
+ assertThat(expectedPropertyParcel).isEqualTo(actualPropertyParcel);
+ } finally {
+ data.recycle();
+ }
+ }
+}
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 59415fb..935c5bf 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.LongSerializer;
import androidx.appsearch.app.StringSerializer;
@@ -228,7 +229,6 @@
* <p>If not specified, defaults to {@link
* AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed
* and cannot be queried).
- * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
*/
@AppSearchSchema.StringPropertyConfig.IndexingType int indexingType()
default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
@@ -540,6 +540,42 @@
}
/**
+ * Configures an {@link EmbeddingVector} field of a class as a property known to AppSearch.
+ */
+ @Documented
+ @Retention(RetentionPolicy.CLASS)
+ @Target({ElementType.FIELD, ElementType.METHOD})
+ @interface EmbeddingProperty {
+ /**
+ * The name of this property. This string is used to query against this property.
+ *
+ * <p>If not specified, the name of the field in the code will be used instead.
+ */
+ String name() default "";
+
+ /**
+ * Configures how a property should be indexed so that it can be retrieved by queries.
+ *
+ * <p>If not specified, defaults to
+ * {@link AppSearchSchema.EmbeddingPropertyConfig#INDEXING_TYPE_NONE} (the field will not be
+ * indexed and cannot be queried).
+ */
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType()
+ default AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+
+ /**
+ * Configures whether this property must be specified for the document to be valid.
+ *
+ * <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#setSchemaAsync schema migration} before
+ * setting this attribute to {@code true}.
+ */
+ boolean required() default false;
+ }
+
+ /**
* Marks a static method or a builder class directly as a builder producer. A builder class
* should contain a "build()" method to construct the AppSearch document object and setter
* methods to set field values.
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 8263467..08a16d2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -44,12 +44,14 @@
* @see AppSearchSession#removeAsync
*/
public final class AppSearchBatchResult<KeyType, ValueType> {
- @NonNull private final Map<KeyType, ValueType> mSuccesses;
+ @NonNull private final Map<KeyType,
+ @androidx.appsearch.checker.nullness.qual.Nullable ValueType> mSuccesses;
@NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
@NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
AppSearchBatchResult(
- @NonNull Map<KeyType, ValueType> successes,
+ @NonNull Map<KeyType, @androidx.appsearch.checker.nullness.qual.Nullable ValueType>
+ successes,
@NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
@NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
mSuccesses = Preconditions.checkNotNull(successes);
@@ -123,7 +125,8 @@
* @param <ValueType> The type of the result objects for successful results.
*/
public static final class Builder<KeyType, ValueType> {
- private ArrayMap<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+ private ArrayMap<KeyType, @androidx.appsearch.checker.nullness.qual.Nullable ValueType>
+ mSuccesses = new ArrayMap<>();
private ArrayMap<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
private boolean mBuilt = false;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
new file mode 100644
index 0000000..aa5326b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.File;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An interface which exposes environment specific methods for AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchEnvironment {
+
+ /** Returns the directory to initialize appsearch based on the environment. */
+ @NonNull
+ File getAppSearchDir(@NonNull Context context, @Nullable UserHandle userHandle);
+
+ /** Returns the correct context for the user based on the environment. */
+ @NonNull
+ Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle);
+
+ /** Returns an ExecutorService based on given parameters. */
+ @NonNull
+ ExecutorService createExecutorService(
+ int corePoolSize,
+ int maxConcurrency,
+ long keepAliveTime,
+ @NonNull TimeUnit unit,
+ @NonNull BlockingQueue<Runnable> workQueue,
+ int priority);
+
+ /** Returns an ExecutorService with a single thread. */
+ @NonNull
+ ExecutorService createSingleThreadExecutor();
+
+ /** Creates and returns an Executor with cached thread pools. */
+ @NonNull
+ ExecutorService createCachedThreadPoolExecutor();
+
+ /**
+ * Returns a cache directory for creating temporary files like in case of migrating documents.
+ */
+ @Nullable
+ File getCacheDir(@NonNull Context context);
+
+ /** Returns if we can log INFO level logs. */
+ boolean isInfoLoggingEnabled();
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
new file mode 100644
index 0000000..9ae8ac1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is a factory class for implementations needed based on the environment.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchEnvironmentFactory {
+ private static volatile AppSearchEnvironment sAppSearchEnvironment;
+
+ /** Returns the singleton instance of {@link AppSearchEnvironment}. */
+ @NonNull
+ public static AppSearchEnvironment getEnvironmentInstance() {
+ AppSearchEnvironment localRef = sAppSearchEnvironment;
+ if (localRef == null) {
+ synchronized (AppSearchEnvironmentFactory.class) {
+ localRef = sAppSearchEnvironment;
+ if (localRef == null) {
+ sAppSearchEnvironment = localRef =
+ new JetpackAppSearchEnvironment();
+ }
+ }
+ }
+ return localRef;
+ }
+
+ /** Sets an instance of {@link AppSearchEnvironment}. for testing.*/
+ @VisibleForTesting
+ public static void setEnvironmentInstanceForTest(
+ @NonNull AppSearchEnvironment appSearchEnvironment) {
+ synchronized (AppSearchEnvironmentFactory.class) {
+ sAppSearchEnvironment = appSearchEnvironment;
+ }
+ }
+
+ private AppSearchEnvironmentFactory() {
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
index d8c79a5..86c8102 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -22,6 +22,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
import androidx.appsearch.util.LogUtil;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
@@ -54,6 +56,7 @@
RESULT_SECURITY_ERROR,
RESULT_DENIED,
RESULT_RATE_LIMITED,
+ RESULT_TIMED_OUT
})
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@@ -101,20 +104,22 @@
/**
* The requested operation is denied for the caller. This error is logged and returned for
* denylist rejections.
- * <!--@exportToFramework:hide-->
*/
- // TODO(b/279047435): unhide this the next time we can make API changes
+ @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
public static final int RESULT_DENIED = 9;
/**
- * The caller has hit AppSearch's rate limit and the requested operation has been rejected.
- * <!--@exportToFramework:hide-->
+ * The caller has hit AppSearch's rate limit and the requested operation has been rejected. The
+ * caller is recommended to reschedule tasks with exponential backoff.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- // TODO(b/279047435): unhide this the next time we can make API changes
+ @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
public static final int RESULT_RATE_LIMITED = 10;
- private final @ResultCode int mResultCode;
+ /** The operation was timed out. */
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ public static final int RESULT_TIMED_OUT = 11;
+
+ @ResultCode private final int mResultCode;
@Nullable private final ValueType mResultValue;
@Nullable private final String mErrorMessage;
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 a4fd79ef..b300d3e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@@ -27,7 +29,15 @@
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.exceptions.IllegalSchemaException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.PropertyConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.AppSearchSchemaCreator;
import androidx.appsearch.util.IndentingStringBuilder;
import androidx.collection.ArraySet;
import androidx.core.util.ObjectsCompat;
@@ -41,6 +51,7 @@
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -52,28 +63,36 @@
*
* @see AppSearchSession#setSchemaAsync
*/
-public final class AppSearchSchema {
- private static final String SCHEMA_TYPE_FIELD = "schemaType";
- private static final String PROPERTIES_FIELD = "properties";
- private static final String PARENT_TYPES_FIELD = "parentTypes";
-
- private final Bundle mBundle;
-
- /** @exportToFramework:hide */
[email protected](creator = "AppSearchSchemaCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class AppSearchSchema extends AbstractSafeParcelable {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public AppSearchSchema(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
- }
-
- /**
- * Returns the {@link Bundle} populated by this builder.
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@NonNull
- public Bundle getBundle() {
- return mBundle;
+ public static final Parcelable.Creator<AppSearchSchema> CREATOR = new AppSearchSchemaCreator();
+
+ @Field(id = 1, getter = "getSchemaType")
+ private final String mSchemaType;
+
+ @Field(id = 2)
+ final List<PropertyConfigParcel> mPropertyConfigParcels;
+
+ @Field(id = 3, getter = "getParentTypes")
+ private final List<String> mParentTypes;
+
+ @Field(id = 4, getter = "getDescription")
+ private final String mDescription;
+
+ @Constructor
+ AppSearchSchema(
+ @Param(id = 1) @NonNull String schemaType,
+ @Param(id = 2) @NonNull List<PropertyConfigParcel> propertyConfigParcels,
+ @Param(id = 3) @NonNull List<String> parentTypes,
+ @Param(id = 4) @NonNull String description) {
+ mSchemaType = Objects.requireNonNull(schemaType);
+ mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);
+ mParentTypes = Objects.requireNonNull(parentTypes);
+ mDescription = Objects.requireNonNull(description);
}
@Override
@@ -88,7 +107,7 @@
* Appends a debugging string for the {@link AppSearchSchema} instance to the given string
* builder.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
Preconditions.checkNotNull(builder);
@@ -96,6 +115,7 @@
builder.append("{\n");
builder.increaseIndentLevel();
builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+ builder.append("description: \"").append(getDescription()).append("\",\n");
builder.append("properties: [\n");
AppSearchSchema.PropertyConfig[] sortedProperties = getProperties()
@@ -121,7 +141,22 @@
/** Returns the name of this schema type, such as Email. */
@NonNull
public String getSchemaType() {
- return mBundle.getString(SCHEMA_TYPE_FIELD, "");
+ return mSchemaType;
+ }
+
+ /**
+ * Returns a natural language description of this schema type.
+ *
+ * <p>Ex. The description for an Email type could be "A type of electronic message".
+ *
+ * <p>This information is purely to help apps consuming this type to understand its semantic
+ * meaning. This field has no effect in AppSearch - it is just stored with the AppSearchSchema.
+ * If {@link Builder#setDescription} is uncalled, then this method will return an empty string.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @NonNull
+ public String getDescription() {
+ return mDescription;
}
/**
@@ -130,35 +165,25 @@
* <p>This method creates a new list when called.
*/
@NonNull
- @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
+ @SuppressWarnings({"MixedMutabilityReturnType"})
public List<PropertyConfig> getProperties() {
- ArrayList<Bundle> propertyBundles =
- mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);
- if (propertyBundles == null || propertyBundles.isEmpty()) {
+ if (mPropertyConfigParcels.isEmpty()) {
return Collections.emptyList();
}
- List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
- for (int i = 0; i < propertyBundles.size(); i++) {
- ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
+ List<PropertyConfig> ret = new ArrayList<>(mPropertyConfigParcels.size());
+ for (int i = 0; i < mPropertyConfigParcels.size(); i++) {
+ ret.add(PropertyConfig.fromParcel(mPropertyConfigParcels.get(i)));
}
return ret;
}
/**
* Returns the list of parent types of this schema for polymorphism.
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline
- * are possible.
- * -->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
@NonNull
public List<String> getParentTypes() {
- List<String> parentTypes = mBundle.getStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD);
- if (parentTypes == null) {
- return Collections.emptyList();
- }
- return Collections.unmodifiableList(parentTypes);
+ return Collections.unmodifiableList(mParentTypes);
}
@Override
@@ -173,6 +198,9 @@
if (!getSchemaType().equals(otherSchema.getSchemaType())) {
return false;
}
+ if (!getDescription().equals(otherSchema.getDescription())) {
+ return false;
+ }
if (!getParentTypes().equals(otherSchema.getParentTypes())) {
return false;
}
@@ -181,21 +209,51 @@
@Override
public int hashCode() {
- return ObjectsCompat.hash(getSchemaType(), getProperties(), getParentTypes());
+ return ObjectsCompat.hash(
+ getSchemaType(),
+ getProperties(),
+ getParentTypes(),
+ getDescription());
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ AppSearchSchemaCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link AppSearchSchema objects}. */
public static final class Builder {
private final String mSchemaType;
- private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
+ private String mDescription = "";
+ private ArrayList<PropertyConfigParcel> mPropertyConfigParcels = new ArrayList<>();
private LinkedHashSet<String> mParentTypes = new LinkedHashSet<>();
private final Set<String> mPropertyNames = new ArraySet<>();
private boolean mBuilt = false;
/** Creates a new {@link AppSearchSchema.Builder}. */
public Builder(@NonNull String schemaType) {
- Preconditions.checkNotNull(schemaType);
- mSchemaType = schemaType;
+ mSchemaType = Preconditions.checkNotNull(schemaType);
+ }
+
+ /**
+ * Sets a natural language description of this schema type.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema#getDescription}.
+ */
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public AppSearchSchema.Builder setDescription(@NonNull String description) {
+ Objects.requireNonNull(description);
+ resetIfBuilt();
+ mDescription = description;
+ return this;
}
/** Adds a property to the given type. */
@@ -208,7 +266,7 @@
if (!mPropertyNames.add(name)) {
throw new IllegalSchemaException("Property defined more than once: " + name);
}
- mPropertyBundles.add(propertyConfig.mBundle);
+ mPropertyConfigParcels.add(propertyConfig.mPropertyConfigParcel);
return this;
}
@@ -273,11 +331,9 @@
*/
@CanIgnoreReturnValue
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SCHEMA_ADD_PARENT_TYPE)
- // @exportToFramework:endStrip()
public AppSearchSchema.Builder addParentType(@NonNull String parentSchemaType) {
Preconditions.checkNotNull(parentSchemaType);
resetIfBuilt();
@@ -288,18 +344,16 @@
/** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
@NonNull
public AppSearchSchema build() {
- Bundle bundle = new Bundle();
- bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
- bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
- bundle.putStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD,
- new ArrayList<>(mParentTypes));
mBuilt = true;
- return new AppSearchSchema(bundle);
+ return new AppSearchSchema(mSchemaType,
+ mPropertyConfigParcels,
+ new ArrayList<>(mParentTypes),
+ mDescription);
}
private void resetIfBuilt() {
if (mBuilt) {
- mPropertyBundles = new ArrayList<>(mPropertyBundles);
+ mPropertyConfigParcels = new ArrayList<>(mPropertyConfigParcels);
mParentTypes = new LinkedHashSet<>(mParentTypes);
mBuilt = false;
}
@@ -313,10 +367,6 @@
* a property.
*/
public abstract static class PropertyConfig {
- static final String NAME_FIELD = "name";
- static final String DATA_TYPE_FIELD = "dataType";
- static final String CARDINALITY_FIELD = "cardinality";
-
/**
* Physical data-types of the contents of the property.
*
@@ -333,28 +383,47 @@
DATA_TYPE_BOOLEAN,
DATA_TYPE_BYTES,
DATA_TYPE_DOCUMENT,
+ DATA_TYPE_EMBEDDING,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface DataType {}
+ public @interface DataType {
+ }
- /** @exportToFramework:hide */
+ /**
+ * Constant value for String data type.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DATA_TYPE_STRING = 1;
- /** @exportToFramework:hide */
+ /**
+ * Constant value for Long data type.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DATA_TYPE_LONG = 2;
- /** @exportToFramework:hide */
+ /**
+ * Constant value for Double data type.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DATA_TYPE_DOUBLE = 3;
- /** @exportToFramework:hide */
+ /**
+ * Constant value for Boolean data type.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DATA_TYPE_BOOLEAN = 4;
/**
* Unstructured BLOB.
+ *
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -364,12 +433,21 @@
* Indicates that the property is itself a {@link GenericDocument}, making it part of a
* hierarchical schema. Any property using this DataType MUST have a valid
* {@link PropertyConfig#getSchemaType}.
+ *
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DATA_TYPE_DOCUMENT = 6;
/**
+ * Indicates that the property is an {@link EmbeddingVector}.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int DATA_TYPE_EMBEDDING = 7;
+
+ /**
* The cardinality of the property (whether it is required, optional or repeated).
*
* <p>NOTE: The integer values of these constants must match the proto enum constants in
@@ -384,7 +462,8 @@
CARDINALITY_REQUIRED,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface Cardinality {}
+ public @interface Cardinality {
+ }
/** Any number of items (including zero) [0...*]. */
public static final int CARDINALITY_REPEATED = 1;
@@ -395,13 +474,10 @@
/** Exactly one value [1]. */
public static final int CARDINALITY_REQUIRED = 3;
- final Bundle mBundle;
+ final PropertyConfigParcel mPropertyConfigParcel;
- @Nullable
- private Integer mHashCode;
-
- PropertyConfig(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
+ PropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ mPropertyConfigParcel = Preconditions.checkNotNull(propertyConfigParcel);
}
@Override
@@ -416,7 +492,7 @@
* Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
* given string builder.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
Preconditions.checkNotNull(builder);
@@ -424,6 +500,7 @@
builder.append("{\n");
builder.increaseIndentLevel();
builder.append("name: \"").append(getName()).append("\",\n");
+ builder.append("description: \"").append(getDescription()).append("\",\n");
if (this instanceof AppSearchSchema.StringPropertyConfig) {
((StringPropertyConfig) this)
@@ -469,6 +546,9 @@
case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
break;
+ case PropertyConfig.DATA_TYPE_EMBEDDING:
+ builder.append("dataType: DATA_TYPE_EMBEDDING,\n");
+ break;
default:
builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
}
@@ -479,7 +559,24 @@
/** Returns the name of this property. */
@NonNull
public String getName() {
- return mBundle.getString(NAME_FIELD, "");
+ return mPropertyConfigParcel.getName();
+ }
+
+ /**
+ * Returns a natural language description of this property.
+ *
+ * <p>Ex. The description for the "homeAddress" property of a "Person" type could be "the
+ * address at which this person lives".
+ *
+ * <p>This information is purely to help apps consuming this type the semantic meaning of
+ * its properties. This field has no effect in AppSearch - it is just stored with the
+ * AppSearchSchema. If the description is not set, then this method will return an empty
+ * string.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @NonNull
+ public String getDescription() {
+ return mPropertyConfigParcel.getDescription();
}
/**
@@ -490,7 +587,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@DataType
public int getDataType() {
- return mBundle.getInt(DATA_TYPE_FIELD, -1);
+ return mPropertyConfigParcel.getDataType();
}
/**
@@ -498,7 +595,7 @@
*/
@Cardinality
public int getCardinality() {
- return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+ return mPropertyConfigParcel.getCardinality();
}
@Override
@@ -510,15 +607,12 @@
return false;
}
PropertyConfig otherProperty = (PropertyConfig) other;
- return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+ return ObjectsCompat.equals(mPropertyConfigParcel, otherProperty.mPropertyConfigParcel);
}
@Override
public int hashCode() {
- if (mHashCode == null) {
- mHashCode = BundleUtil.deepHashCode(mBundle);
- }
- return mHashCode;
+ return mPropertyConfigParcel.hashCode();
}
/**
@@ -528,41 +622,40 @@
* <p>The bundle is not cloned.
*
* @throws IllegalArgumentException if the bundle does no contain a recognized
- * value in its {@code DATA_TYPE_FIELD}.
+ * value in its {@code DATA_TYPE_FIELD}.
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
- switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
+ public static PropertyConfig fromParcel(
+ @NonNull PropertyConfigParcel propertyConfigParcel) {
+ Preconditions.checkNotNull(propertyConfigParcel);
+ switch (propertyConfigParcel.getDataType()) {
case PropertyConfig.DATA_TYPE_STRING:
- return new StringPropertyConfig(propertyBundle);
+ return new StringPropertyConfig(propertyConfigParcel);
case PropertyConfig.DATA_TYPE_LONG:
- return new LongPropertyConfig(propertyBundle);
+ return new LongPropertyConfig(propertyConfigParcel);
case PropertyConfig.DATA_TYPE_DOUBLE:
- return new DoublePropertyConfig(propertyBundle);
+ return new DoublePropertyConfig(propertyConfigParcel);
case PropertyConfig.DATA_TYPE_BOOLEAN:
- return new BooleanPropertyConfig(propertyBundle);
+ return new BooleanPropertyConfig(propertyConfigParcel);
case PropertyConfig.DATA_TYPE_BYTES:
- return new BytesPropertyConfig(propertyBundle);
+ return new BytesPropertyConfig(propertyConfigParcel);
case PropertyConfig.DATA_TYPE_DOCUMENT:
- return new DocumentPropertyConfig(propertyBundle);
+ return new DocumentPropertyConfig(propertyConfigParcel);
+ case PropertyConfig.DATA_TYPE_EMBEDDING:
+ return new EmbeddingPropertyConfig(propertyConfigParcel);
default:
throw new IllegalArgumentException(
"Unsupported property bundle of type "
- + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
- + "; contents: " + propertyBundle);
+ + propertyConfigParcel.getDataType()
+ + "; contents: " + propertyConfigParcel);
}
}
}
/** Configuration for a property of type String in a Document. */
public static final class StringPropertyConfig extends PropertyConfig {
- private static final String INDEXING_TYPE_FIELD = "indexingType";
- private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
- private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType";
- private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation";
-
/**
* Encapsulates the configurations on how AppSearch should query/index these terms.
* @exportToFramework:hide
@@ -574,7 +667,8 @@
INDEXING_TYPE_PREFIXES,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface IndexingType {}
+ public @interface IndexingType {
+ }
/** Content in this property will not be tokenized or indexed. */
public static final int INDEXING_TYPE_NONE = 0;
@@ -611,7 +705,8 @@
TOKENIZER_TYPE_RFC822
})
@Retention(RetentionPolicy.SOURCE)
- public @interface TokenizerType {}
+ public @interface TokenizerType {
+ }
/**
* This value indicates that no tokens should be extracted from this property.
@@ -646,11 +741,9 @@
* <p>It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is
* {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
*/
-// @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.VERBATIM_SEARCH)
-// @exportToFramework:endStrip()
public static final int TOKENIZER_TYPE_VERBATIM = 2;
/**
@@ -663,11 +756,9 @@
* <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
* {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
*/
-// @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.TOKENIZER_TYPE_RFC822)
-// @exportToFramework:endStrip()
public static final int TOKENIZER_TYPE_RFC822 = 3;
/**
@@ -684,7 +775,8 @@
})
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
- public @interface JoinableValueType {}
+ public @interface JoinableValueType {
+ }
/** Content in this property is not joinable. */
public static final int JOINABLE_VALUE_TYPE_NONE = 0;
@@ -700,27 +792,37 @@
* {@link PropertyConfig#CARDINALITY_REQUIRED}.
* </ul>
*/
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
- // @exportToFramework:endStrip()
public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1;
- StringPropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ StringPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Returns how the property is indexed. */
- @IndexingType
+ @StringPropertyConfig.IndexingType
public int getIndexingType() {
- return mBundle.getInt(INDEXING_TYPE_FIELD);
+ StringIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getStringIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return INDEXING_TYPE_NONE;
+ }
+
+ return indexingConfigParcel.getIndexingType();
}
/** Returns how this property is tokenized (split into words). */
@TokenizerType
public int getTokenizerType() {
- return mBundle.getInt(TOKENIZER_TYPE_FIELD);
+ StringIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getStringIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return TOKENIZER_TYPE_NONE;
+ }
+
+ return indexingConfigParcel.getTokenizerType();
}
/**
@@ -728,27 +830,27 @@
*/
@JoinableValueType
public int getJoinableValueType() {
- return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE);
- }
+ JoinableConfigParcel joinableConfigParcel = mPropertyConfigParcel
+ .getJoinableConfigParcel();
+ if (joinableConfigParcel == null) {
+ return JOINABLE_VALUE_TYPE_NONE;
+ }
- /**
- * Returns whether or not documents in this schema should be deleted when the document
- * referenced by this field is deleted.
- *
- * @see JoinSpec
- * @<!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()hide-->
- */
- public boolean getDeletionPropagation() {
- return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false);
+ return joinableConfigParcel.getJoinableValueType();
}
/** Builder for {@link StringPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
- @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
- @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;
- @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
+ @StringPropertyConfig.IndexingType
+ private int mIndexingType = INDEXING_TYPE_NONE;
+ @TokenizerType
+ private int mTokenizerType = TOKENIZER_TYPE_NONE;
+ @JoinableValueType
+ private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
private boolean mDeletionPropagation = false;
/** Creates a new {@link StringPropertyConfig.Builder}. */
@@ -757,6 +859,24 @@
}
/**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public StringPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -781,7 +901,8 @@
*/
@CanIgnoreReturnValue
@NonNull
- public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+ public StringPropertyConfig.Builder setIndexingType(
+ @StringPropertyConfig.IndexingType int indexingType) {
Preconditions.checkArgumentInRange(
indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
mIndexingType = indexingType;
@@ -830,25 +951,6 @@
}
/**
- * Configures whether or not documents in this schema will be removed when the document
- * referred to by this property is deleted.
- *
- * <p> Requires that a joinable value type is set.
- * @<!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()hide-->
- */
- @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation
- @NonNull
- // @exportToFramework:startStrip()
- @RequiresFeature(
- enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
- name = Features.SCHEMA_SET_DELETION_PROPAGATION)
- // @exportToFramework:endStrip()
- public Builder setDeletionPropagation(boolean deletionPropagation) {
- mDeletionPropagation = deletionPropagation;
- return this;
- }
-
- /**
* Constructs a new {@link StringPropertyConfig} from the contents of this builder.
*/
@NonNull
@@ -868,15 +970,17 @@
Preconditions.checkState(!mDeletionPropagation, "Cannot set deletion "
+ "propagation without setting a joinable value type");
}
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
- bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
- bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType);
- bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation);
- return new StringPropertyConfig(bundle);
+ PropertyConfigParcel.StringIndexingConfigParcel stringConfigParcel =
+ new StringIndexingConfigParcel(mIndexingType, mTokenizerType);
+ JoinableConfigParcel joinableConfigParcel =
+ new JoinableConfigParcel(mJoinableValueType, mDeletionPropagation);
+ return new StringPropertyConfig(
+ PropertyConfigParcel.createForString(
+ mPropertyName,
+ mDescription,
+ mCardinality,
+ stringConfigParcel,
+ joinableConfigParcel));
}
}
@@ -886,7 +990,7 @@
*
* <p>This appends fields specific to a {@link StringPropertyConfig} instance.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
switch (getIndexingType()) {
@@ -935,11 +1039,10 @@
/** Configuration for a property containing a 64-bit integer. */
public static final class LongPropertyConfig extends PropertyConfig {
- private static final String INDEXING_TYPE_FIELD = "indexingType";
-
/**
* Encapsulates the configurations on how AppSearch should query/index these 64-bit
* integers.
+ *
* @exportToFramework:hide
*/
@IntDef(value = {
@@ -948,7 +1051,8 @@
})
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
- public @interface IndexingType {}
+ public @interface IndexingType {
+ }
/** Content in this property will not be indexed. */
public static final int INDEXING_TYPE_NONE = 0;
@@ -959,28 +1063,34 @@
*
* <p>For example, a property with 1024 should match numeric search range query [0, 2000].
*/
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.NUMERIC_SEARCH)
- // @exportToFramework:endStrip()
public static final int INDEXING_TYPE_RANGE = 1;
- LongPropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ LongPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Returns how the property is indexed. */
- @IndexingType
+ @LongPropertyConfig.IndexingType
public int getIndexingType() {
- return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
+ PropertyConfigParcel.IntegerIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getIntegerIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return INDEXING_TYPE_NONE;
+ }
+ return indexingConfigParcel.getIndexingType();
}
/** Builder for {@link LongPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
- @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
+ @LongPropertyConfig.IndexingType
+ private int mIndexingType = INDEXING_TYPE_NONE;
/** Creates a new {@link LongPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -988,6 +1098,24 @@
}
/**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public LongPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -1012,7 +1140,8 @@
*/
@CanIgnoreReturnValue
@NonNull
- public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+ public LongPropertyConfig.Builder setIndexingType(
+ @LongPropertyConfig.IndexingType int indexingType) {
Preconditions.checkArgumentInRange(
indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType");
mIndexingType = indexingType;
@@ -1022,12 +1151,9 @@
/** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
@NonNull
public LongPropertyConfig build() {
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
- return new LongPropertyConfig(bundle);
+ return new LongPropertyConfig(
+ PropertyConfigParcel.createForLong(
+ mPropertyName, mDescription, mCardinality, mIndexingType));
}
}
@@ -1037,7 +1163,7 @@
*
* <p>This appends fields specific to a {@link LongPropertyConfig} instance.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
switch (getIndexingType()) {
@@ -1055,14 +1181,16 @@
/** Configuration for a property containing a double-precision decimal number. */
public static final class DoublePropertyConfig extends PropertyConfig {
- DoublePropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ DoublePropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Builder for {@link DoublePropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link DoublePropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -1070,6 +1198,24 @@
}
/**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public DoublePropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -1088,25 +1234,25 @@
/** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
@NonNull
public DoublePropertyConfig build() {
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- return new DoublePropertyConfig(bundle);
+ return new DoublePropertyConfig(
+ PropertyConfigParcel.createForDouble(
+ mPropertyName, mDescription, mCardinality));
}
}
}
/** Configuration for a property containing a boolean. */
public static final class BooleanPropertyConfig extends PropertyConfig {
- BooleanPropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ BooleanPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Builder for {@link BooleanPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link BooleanPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -1114,6 +1260,24 @@
}
/**
+ Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public BooleanPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -1132,25 +1296,25 @@
/** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
@NonNull
public BooleanPropertyConfig build() {
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- return new BooleanPropertyConfig(bundle);
+ return new BooleanPropertyConfig(
+ PropertyConfigParcel.createForBoolean(
+ mPropertyName, mDescription, mCardinality));
}
}
}
/** Configuration for a property containing a byte array. */
public static final class BytesPropertyConfig extends PropertyConfig {
- BytesPropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ BytesPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Builder for {@link BytesPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link BytesPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -1158,6 +1322,24 @@
}
/**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public BytesPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -1178,30 +1360,23 @@
*/
@NonNull
public BytesPropertyConfig build() {
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- return new BytesPropertyConfig(bundle);
+ return new BytesPropertyConfig(
+ PropertyConfigParcel.createForBytes(
+ mPropertyName, mDescription, mCardinality));
}
}
}
/** Configuration for a property containing another Document. */
public static final class DocumentPropertyConfig extends PropertyConfig {
- private static final String SCHEMA_TYPE_FIELD = "schemaType";
- private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
- private static final String INDEXABLE_NESTED_PROPERTIES_LIST_FIELD =
- "indexableNestedPropertiesList";
-
- DocumentPropertyConfig(@NonNull Bundle bundle) {
- super(bundle);
+ DocumentPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
}
/** Returns the logical schema-type of the contents of this document property. */
@NonNull
public String getSchemaType() {
- return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+ return Preconditions.checkNotNull(mPropertyConfigParcel.getSchemaType());
}
/**
@@ -1215,24 +1390,33 @@
* indexing a subset of properties from the nested document.
*/
public boolean shouldIndexNestedProperties() {
- return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
+ DocumentIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return false;
+ }
+
+ return indexingConfigParcel.shouldIndexNestedProperties();
}
/**
* Returns the list of indexable nested properties for the nested document.
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
- * Mainline are possible.
- * -->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
@NonNull
public List<String> getIndexableNestedProperties() {
+ DocumentIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return Collections.emptyList();
+ }
+
List<String> indexableNestedPropertiesList =
- mBundle.getStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD);
+ indexingConfigParcel.getIndexableNestedPropertiesList();
if (indexableNestedPropertiesList == null) {
return Collections.emptyList();
}
+
return Collections.unmodifiableList(indexableNestedPropertiesList);
}
@@ -1240,7 +1424,9 @@
public static final class Builder {
private final String mPropertyName;
private final String mSchemaType;
- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
private boolean mShouldIndexNestedProperties = false;
private final Set<String> mIndexableNestedPropertiesList = new ArraySet<>();
@@ -1250,9 +1436,9 @@
* @param propertyName The logical name of the property in the schema, which will be
* used as the key for this property in
* {@link GenericDocument.Builder#setPropertyDocument}.
- * @param schemaType The type of documents which will be stored in this property.
- * Documents of different types cannot be mixed into a single
- * property.
+ * @param schemaType The type of documents which will be stored in this property.
+ * Documents of different types cannot be mixed into a single
+ * property.
*/
public Builder(@NonNull String propertyName, @NonNull String schemaType) {
mPropertyName = Preconditions.checkNotNull(propertyName);
@@ -1260,6 +1446,24 @@
}
/**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public DocumentPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
* Sets the cardinality of the property (whether it is optional, required or repeated).
*
* <p>If this method is not called, the default cardinality is
@@ -1297,19 +1501,13 @@
* Adds one or more properties for indexing from the nested document property.
*
* @see #addIndexableNestedProperties(Collection)
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
- * Mainline are possible.
- * -->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
@CanIgnoreReturnValue
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
- // @exportToFramework:endStrip()
public DocumentPropertyConfig.Builder addIndexableNestedProperties(
@NonNull String... indexableNestedProperties) {
Preconditions.checkNotNull(indexableNestedProperties);
@@ -1320,20 +1518,14 @@
* Adds one or more property paths for indexing from the nested document property.
*
* @see #addIndexableNestedProperties(Collection)
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
- * Mainline are possible.
- * -->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
- // @exportToFramework:endStrip()
public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(
@NonNull PropertyPath... indexableNestedPropertyPaths) {
Preconditions.checkNotNull(indexableNestedPropertyPaths);
@@ -1371,11 +1563,9 @@
*/
@CanIgnoreReturnValue
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
- // @exportToFramework:endStrip()
public DocumentPropertyConfig.Builder addIndexableNestedProperties(
@NonNull Collection<String> indexableNestedProperties) {
Preconditions.checkNotNull(indexableNestedProperties);
@@ -1387,20 +1577,14 @@
* Adds one or more property paths for indexing from the nested document property.
*
* @see #addIndexableNestedProperties(Collection)
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
- * Mainline are possible.
- * -->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
- // @exportToFramework:endStrip()
public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(
@NonNull Collection<PropertyPath> indexableNestedPropertyPaths) {
Preconditions.checkNotNull(indexableNestedPropertyPaths);
@@ -1426,15 +1610,14 @@
+ "to be false when one or more indexableNestedProperties are "
+ "provided.");
}
- Bundle bundle = new Bundle();
- bundle.putString(NAME_FIELD, mPropertyName);
- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
- bundle.putInt(CARDINALITY_FIELD, mCardinality);
- bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
- bundle.putStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD,
- new ArrayList<>(mIndexableNestedPropertiesList));
- bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);
- return new DocumentPropertyConfig(bundle);
+ return new DocumentPropertyConfig(
+ PropertyConfigParcel.createForDocument(
+ mPropertyName,
+ mDescription,
+ mCardinality,
+ mSchemaType,
+ new DocumentIndexingConfigParcel(mShouldIndexNestedProperties,
+ new ArrayList<>(mIndexableNestedPropertiesList))));
}
}
@@ -1444,7 +1627,7 @@
*
* <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
builder
@@ -1459,4 +1642,133 @@
builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
}
}
+
+ /**
+ * Configuration for a property of type {@link EmbeddingVector} in a Document.
+ */
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public static final class EmbeddingPropertyConfig extends PropertyConfig {
+ /**
+ * Encapsulates the configurations on how AppSearch should query/index these embedding
+ * vectors.
+ *
+ * @exportToFramework:hide
+ */
+ @IntDef(value = {
+ INDEXING_TYPE_NONE,
+ INDEXING_TYPE_SIMILARITY
+ })
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IndexingType {
+ }
+
+ /** Content in this property will not be indexed. */
+ public static final int INDEXING_TYPE_NONE = 0;
+
+ /**
+ * Embedding vectors in this property will be indexed.
+ *
+ * <p>The index offers 100% accuracy, but has linear time complexity based on the number
+ * of embedding vectors within the index.
+ */
+ public static final int INDEXING_TYPE_SIMILARITY = 1;
+
+ EmbeddingPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+ super(propertyConfigParcel);
+ }
+
+ /** Returns how the property is indexed. */
+ @EmbeddingPropertyConfig.IndexingType
+ public int getIndexingType() {
+ PropertyConfigParcel.EmbeddingIndexingConfigParcel indexingConfigParcel =
+ mPropertyConfigParcel.getEmbeddingIndexingConfigParcel();
+ if (indexingConfigParcel == null) {
+ return INDEXING_TYPE_NONE;
+ }
+ return indexingConfigParcel.getIndexingType();
+ }
+
+ /** Builder for {@link EmbeddingPropertyConfig}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public static final class Builder {
+ private final String mPropertyName;
+ private String mDescription = "";
+ @Cardinality
+ private int mCardinality = CARDINALITY_OPTIONAL;
+ @EmbeddingPropertyConfig.IndexingType
+ private int mIndexingType = INDEXING_TYPE_NONE;
+
+ /** Creates a new {@link EmbeddingPropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Preconditions.checkNotNull(propertyName);
+ }
+
+ /**
+ * Sets a natural language description of this property.
+ *
+ * <p> For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_SET_DESCRIPTION)
+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public EmbeddingPropertyConfig.Builder setDescription(@NonNull String description) {
+ mDescription = Objects.requireNonNull(description);
+ return this;
+ }
+
+ /**
+ * Sets the cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is
+ * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public EmbeddingPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /**
+ * Configures how a property should be indexed so that it can be retrieved by queries.
+ *
+ * <p>If this method is not called, the default indexing type is
+ * {@link EmbeddingPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed
+ * and cannot be matched by queries.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public EmbeddingPropertyConfig.Builder setIndexingType(
+ @EmbeddingPropertyConfig.IndexingType int indexingType) {
+ Preconditions.checkArgumentInRange(
+ indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_SIMILARITY,
+ "indexingType");
+ mIndexingType = indexingType;
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link EmbeddingPropertyConfig} from the contents of this
+ * builder.
+ */
+ @NonNull
+ public EmbeddingPropertyConfig build() {
+ return new EmbeddingPropertyConfig(
+ PropertyConfigParcel.createForEmbedding(
+ mPropertyName, mDescription, mCardinality, mIndexingType));
+ }
+ }
+ }
}
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 05d1815..58821b7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -176,7 +176,7 @@
* <p>The newly added custom functions covered by this feature are:
* <ul>
* <li>createList(String...)</li>
- * <li>search(String, List<String>)</li>
+ * <li>search(String, {@code List<String>})</li>
* <li>propertyDefined(String)</li>
* </ul>
*
@@ -187,13 +187,14 @@
* query language and an optional list of strings that specify the properties to be
* restricted to. This exists as a convenience for multiple property restricts. So,
* for example, the query `(subject:foo OR body:foo) (subject:bar OR body:bar)`
- * could be rewritten as `search("foo bar", createList("subject", "bar"))`.
+ * could be rewritten as `search("foo bar", createList("subject", "body"))`.
*
* <p>propertyDefined takes a string specifying the property of interest and matches all
* documents of any type that defines the specified property
* (ex. `propertyDefined("sender.name")`). Note that propertyDefined will match so long as
- * the document's type defines the specified property. It does NOT require that the document
- * actually hold any values for this property.
+ * the document's type defines the specified property. Unlike the "hasProperty" function
+ * below, this function does NOT require that the document actually hold any values for this
+ * property.
*
* <p>{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
* query language, the values of properties that have
@@ -209,6 +210,68 @@
*
* <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
*
+ * <p>{@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}: This feature covers the
+ * "hasProperty" function in query expressions, which takes a string specifying the property
+ * of interest and matches all documents that hold values for this property. Not to be
+ * confused with the "propertyDefined" function, which checks whether a document's schema
+ * has defined the property, instead of whether a document itself has this property.
+ *
+ * <p>Ex. `foo hasProperty("sender.name")` will return all documents that have the term "foo"
+ * AND have values in the property "sender.name". Consider two documents, documentA and
+ * documentB, of the same schema with an optional property "sender.name". If documentA sets
+ * "foo" in this property but documentB does not, then `hasProperty("sender.name")` will only
+ * match documentA. However, `propertyDefined("sender.name")` will match both documentA and
+ * documentB, regardless of whether a value is actually set.
+ *
+ * <p>{@link Features#SCHEMA_EMBEDDING_PROPERTY_CONFIG}: This feature covers the
+ * "semanticSearch" and "getSearchSpecEmbedding" functions in query expressions, which are
+ * used for semantic search.
+ *
+ * <p>Usage: semanticSearch(getSearchSpecEmbedding({embedding_index}), {low}, {high}, {metric})
+ * <ul>
+ * <li>semanticSearch matches all documents that have at least one embedding vector with
+ * a matching model signature (see {@link EmbeddingVector#getModelSignature()}) and a
+ * similarity score within the range specified based on the provided metric.</li>
+ * <li>getSearchSpecEmbedding({embedding_index}) retrieves the embedding search passed in
+ * {@link SearchSpec.Builder#addSearchEmbeddings} based on the index specified, which
+ * starts from 0.</li>
+ * <li>"low" and "high" are floating point numbers that specify the similarity score
+ * range. If omitted, they default to negative and positive infinity, respectively.</li>
+ * <li>"metric" is a string value that specifies how embedding similarities should be
+ * calculated. If omitted, it defaults to the metric specified in
+ * {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. Possible
+ * values:</li>
+ * <ul>
+ * <li>"COSINE"</li>
+ * <li>"DOT_PRODUCT"</li>
+ * <li>"EUCLIDEAN"</li>
+ * </ul>
+ * </ul>
+ *
+ * <p>Examples:
+ * <ul>
+ * <li>Basic: semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")</li>
+ * <li>With a property restriction:
+ * property1:semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)</li>
+ * <li>Hybrid: foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)</li>
+ * <li>Complex: (foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)) AND bar</li>
+ * </ul>
+ *
+ * <p>{@link Features#LIST_FILTER_TOKENIZE_FUNCTION}: This feature covers the
+ * "tokenize" function in query expressions, which takes a string and treats the entire string
+ * as plain text. This string is then segmented, normalized and stripped of punctuation-only
+ * segments. The remaining tokens are then AND'd together. This function is useful for callers
+ * who wish to provide user input, but want to ensure that that user input does not invoke any
+ * query operators.
+ *
+ * <p>Ex. `foo OR tokenize("bar OR baz.")`. The string "bar OR baz." will be segmented into
+ * "bar", "OR", "baz", ".". Punctuation is removed and the segments are normalized to "bar",
+ * "or", "baz". This query will be equivalent to `foo OR (bar AND or AND baz)`.
+ *
+ * <p>Ex. `tokenize("\"bar\" OR \\baz")`. Quotation marks and escape characters must be escaped.
+ * This query will be segmented into "\"", "bar", "\"", "OR", "\", "baz". Once stripped of
+ * punctuation and normalized, this will be equivalent to the query `bar AND or AND baz`.
+ *
* <p>The availability of each of these features can be checked by calling
* {@link Features#isFeatureSupported} with the desired feature.
*
@@ -223,6 +286,8 @@
* match type, etc.
* @return a {@link SearchResults} object for retrieved matched documents.
*/
+ // TODO(b/326656531): Refine the javadoc to provide guidance on the best practice of
+ // embedding searches and how to select an appropriate metric.
@NonNull
SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
new file mode 100644
index 0000000..1addca7
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingVectorCreator;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Embeddings are vector representations of data, such as text, images, and audio, which can be
+ * generated by machine learning models and used for semantic search. This class represents an
+ * embedding vector, which wraps a float array for the values of the embedding vector and a model
+ * signature that can be any string to distinguish between embedding vectors generated by
+ * different models.
+ *
+ * <p>For more details on how embedding search works, check {@link AppSearchSession#search} and
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+ *
+ * @see SearchSpec.Builder#addSearchEmbeddings
+ * @see GenericDocument.Builder#setPropertyEmbedding
+ */
+@RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+@FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
[email protected](creator = "EmbeddingVectorCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class EmbeddingVector extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static final Parcelable.Creator<EmbeddingVector> CREATOR =
+ new EmbeddingVectorCreator();
+ @NonNull
+ @Field(id = 1, getter = "getValues")
+ private final float[] mValues;
+ @NonNull
+ @Field(id = 2, getter = "getModelSignature")
+ private final String mModelSignature;
+ @Nullable
+ private Integer mHashCode;
+
+ /**
+ * Creates a new {@link EmbeddingVector}.
+ *
+ * @throws IllegalArgumentException if {@code values} is empty.
+ */
+ @Constructor
+ public EmbeddingVector(
+ @Param(id = 1) @NonNull float[] values,
+ @Param(id = 2) @NonNull String modelSignature) {
+ mValues = Preconditions.checkNotNull(values);
+ if (mValues.length == 0) {
+ throw new IllegalArgumentException("Embedding values cannot be empty.");
+ }
+ mModelSignature = Preconditions.checkNotNull(modelSignature);
+ }
+
+ /**
+ * Returns the values of this embedding vector.
+ */
+ @NonNull
+ public float[] getValues() {
+ return mValues;
+ }
+
+ /**
+ * Returns the model signature of this embedding vector, which is an arbitrary string to
+ * distinguish between embedding vectors generated by different models.
+ */
+ @NonNull
+ public String getModelSignature() {
+ return mModelSignature;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null) return false;
+ if (!(o instanceof EmbeddingVector)) return false;
+ EmbeddingVector that = (EmbeddingVector) o;
+ return Arrays.equals(mValues, that.mValues)
+ && mModelSignature.equals(that.mModelSignature);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = Objects.hash(Arrays.hashCode(mValues), mModelSignature);
+ }
+ return mHashCode;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ EmbeddingVectorCreator.writeToParcel(this, dest, flags);
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
new file mode 100644
index 0000000..eaaec59
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Provides a connection to all enterprise (work profile) AppSearch databases the querying
+ * application has been granted access to.
+ *
+ * <p>This session can be created from any user profile but will only properly return results when
+ * created from the main profile. If the user is not the main profile or an associated work profile
+ * does not exist, queries will still successfully complete but with empty results.
+ *
+ * <p>Schemas must be explicitly tagged enterprise and may require additional permissions to be
+ * visible from an enterprise session. Retrieved documents may also have certain fields restricted
+ * or modified unlike if they were retrieved directly from {@link GlobalSearchSession} on the work
+ * profile.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see GlobalSearchSession
+ */
+@RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ENTERPRISE_GLOBAL_SEARCH_SESSION)
+public interface EnterpriseGlobalSearchSession {
+ /**
+ * 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 EnterpriseGlobalSearchSession} 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
+ ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request);
+
+ /**
+ * Retrieves documents from all enterprise (work profile) AppSearch databases that the querying
+ * application has access to.
+ *
+ * <p>Applications can be granted access to documents by specifying
+ * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or
+ * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.
+ *
+ * <p>Document access can also be granted to system UIs by specifying
+ * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or
+ * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem}
+ * when building a schema.
+ *
+ * <p>See {@link AppSearchSession#search} for a detailed explanation on
+ * forming a query string.
+ *
+ * <p>This method is lightweight. The heavy work will be done in
+ * {@link SearchResults#getNextPageAsync}.
+ *
+ * @param queryExpression query string to search.
+ * @param searchSpec spec for setting document filters, adding projection, setting term
+ * match type, etc.
+ * @return a {@link SearchResults} object for retrieved matched documents.
+ */
+ @NonNull
+ SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+ /**
+ * 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
+ ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+ @NonNull String databaseName);
+
+ /**
+ * Returns the {@link Features} to check for the availability of certain features
+ * for this session.
+ */
+ @NonNull
+ Features getFeatures();
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
index 9fb1df0..c89fb1e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
@@ -27,13 +27,25 @@
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface FeatureConstants {
+public final class FeatureConstants {
/** Feature constants for {@link Features#NUMERIC_SEARCH}. */
- String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+ public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
/** Feature constants for {@link Features#VERBATIM_SEARCH}. */
- String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+ public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
/** Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}. */
- String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+ public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+
+ /** Feature constants for {@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}. */
+ public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION =
+ "LIST_FILTER_HAS_PROPERTY_FUNCTION";
+
+ /** A feature constant for the "semanticSearch" function in {@link AppSearchSession#search}. */
+ public static final String EMBEDDING_SEARCH = "EMBEDDING_SEARCH";
+
+ /** A feature constant for the "tokenize" function in {@link AppSearchSession#search}. */
+ public static final String LIST_FILTER_TOKENIZE_FUNCTION = "TOKENIZE";
+
+ private FeatureConstants() {}
}
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 bfc4deb..e89dc99 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -16,7 +16,8 @@
package androidx.appsearch.app;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
+
+import java.util.Set;
/**
* A class that encapsulates all features that are only supported in certain cases (e.g. only on
@@ -29,6 +30,8 @@
*/
// @exportToFramework:copyToPath(../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java)
+// Note: When adding new fields, The @RequiresFeature is needed in setters but could be skipped in
+// getters if call the getter won't send unsupported requests to the AppSearch-framework-impl.
public interface Features {
/**
@@ -60,8 +63,8 @@
/**
* Feature for {@link #isFeatureSupported(String)}. This feature covers
- * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},
- * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)},
+ * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility(String)},
* {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()},
* {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()},
* {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},
@@ -84,6 +87,7 @@
* <p>For details on the numeric search expressions in the query language, see
* {@link AppSearchSession#search}.
*/
+ // Note: The preferred name of this feature should have been LIST_FILTER_NUMERIC_SEARCH.
String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH;
/**
@@ -94,6 +98,7 @@
*
* <p>For details on the verbatim string operator, see {@link AppSearchSession#search}.
*/
+ // Note: The preferred name of this feature should have been LIST_FILTER_VERBATIM_SEARCH.
String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH;
/**
@@ -106,6 +111,42 @@
String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE;
/**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the
+ * "hasProperty" function in query expressions.
+ *
+ * <p>For details on the "hasProperty" function in the query language, see
+ * {@link AppSearchSession#search}.
+ */
+ String LIST_FILTER_HAS_PROPERTY_FUNCTION = FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the
+ * "tokenize" function in query expressions.
+ *
+ * <p>For details on the "tokenize" function in the query language, see
+ * {@link AppSearchSession#search}.
+ */
+ String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers whether or not the
+ * AppSearch backend can store the descriptions returned by
+ * {@link AppSearchSchema#getDescription} and
+ * {@link AppSearchSchema.PropertyConfig#getDescription}.
+ */
+ String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link AppSearchSchema.EmbeddingPropertyConfig}.
+ *
+ * <p>For details on the embedding search expressions, see {@link AppSearchSession#search} for
+ * the query language and {@link SearchSpec.Builder#setRankingStrategy(String)} for the ranking
+ * language.
+ */
+ String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+
+ /**
* Feature for {@link #isFeatureSupported(String)}. This feature covers
* {@link SearchSpec#GROUPING_TYPE_PER_SCHEMA}
*/
@@ -119,10 +160,9 @@
/**
* Feature for {@link #isFeatureSupported(String)}. This feature covers
- * {@link SearchSpec.Builder#addFilterProperties}.
- * @exportToFramework:hide
+ * {@link SearchSpec.Builder#addFilterProperties} and
+ * {@link SearchSuggestionSpec.Builder#addFilterProperties}.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
/**
@@ -145,12 +185,6 @@
String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
/**
- * Feature for {@link #isFeatureSupported(String)}. This feature covers
- * {@link AppSearchSchema.StringPropertyConfig.Builder#setDeletionPropagation}.
- */
- String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
-
- /**
* Feature for {@link #isFeatureSupported(String)}. This feature covers setting schemas with
* circular references for {@link AppSearchSession#setSchemaAsync}.
*/
@@ -170,6 +204,38 @@
String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
/**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SearchSpec.Builder#setSearchSourceLogTag(String)}.
+ */
+ String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SetSchemaRequest.Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}.
+ */
+ String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig}.
+ */
+ String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG =
+ "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link EnterpriseGlobalSearchSession}
+ */
+ String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SearchSpec.Builder#addInformationalRankingExpressions}.
+ */
+ String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS =
+ "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
+
+ /**
* Returns whether a feature is supported at run-time. Feature support depends on the
* feature in question, the AppSearch backend being used and the Android version of the
* device.
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 57134edba..6de5448 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -17,8 +17,6 @@
package androidx.appsearch.app;
import android.annotation.SuppressLint;
-import android.os.Bundle;
-import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.IntRange;
@@ -27,9 +25,11 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.annotation.Document;
-import androidx.appsearch.app.PropertyPath.PathSegment;
import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.safeparcel.PropertyParcel;
import androidx.appsearch.util.IndentingStringBuilder;
import androidx.core.util.Preconditions;
@@ -39,6 +39,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -64,23 +65,9 @@
/** The maximum number of indexed properties a document can have. */
private static final int MAX_INDEXED_PROPERTIES = 16;
- /** The default score of document. */
- private static final int DEFAULT_SCORE = 0;
-
- /** The default time-to-live in millisecond of a document, which is infinity. */
- private static final long DEFAULT_TTL_MILLIS = 0L;
-
- private static final String PROPERTIES_FIELD = "properties";
- private static final String BYTE_ARRAY_FIELD = "byteArray";
- private static final String SCHEMA_TYPE_FIELD = "schemaType";
- private static final String ID_FIELD = "id";
- private static final String SCORE_FIELD = "score";
- private static final String TTL_MILLIS_FIELD = "ttlMillis";
- private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
- private static final String NAMESPACE_FIELD = "namespace";
- private static final String PARENT_TYPES_FIELD = "parentTypes";
-
/**
+ * Fixed constant synthetic property for parent types.
+ *
* <!--@exportToFramework:hide-->
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -104,6 +91,7 @@
* {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}.
*
* <!--@exportToFramework:ifJetpack()-->
+ *
* @deprecated This is no longer a static value, but depends on SDK version and what AppSearch
* implementation is being used. Use {@link Features#getMaxIndexedProperties} instead.
* <!--@exportToFramework:else()-->
@@ -138,43 +126,20 @@
}
// @exportToFramework:endStrip()
- /**
- * Contains all {@link GenericDocument} information in a packaged format.
- *
- * <p>Keys are the {@code *_FIELD} constants in this class.
- */
- @NonNull
- final Bundle mBundle;
-
- /** Contains all properties in {@link GenericDocument} to support getting properties via name */
- @NonNull
- private final Bundle mProperties;
-
- @NonNull
- private final String mId;
- @NonNull
- private final String mSchemaType;
- private final long mCreationTimestampMillis;
- @Nullable
- private Integer mHashCode;
+ /** The class to hold all meta data and properties for this {@link GenericDocument}. */
+ private final GenericDocumentParcel mDocumentParcel;
/**
- * Rebuilds a {@link GenericDocument} from a bundle.
+ * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}.
*
- * @param bundle Packaged {@link GenericDocument} data, such as the result of
- * {@link #getBundle}.
+ * @param documentParcel Packaged {@link GenericDocument} data, such as the result of
+ * {@link #getDocumentParcel()}.
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressWarnings("deprecation")
- public GenericDocument(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
- mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
- mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD));
- mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
- mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
- System.currentTimeMillis());
+ public GenericDocument(@NonNull GenericDocumentParcel documentParcel) {
+ mDocumentParcel = Objects.requireNonNull(documentParcel);
}
/**
@@ -183,36 +148,37 @@
* <p>This method should be only used by constructor of a subclass.
*/
protected GenericDocument(@NonNull GenericDocument document) {
- this(document.mBundle);
+ this(document.mDocumentParcel);
}
/**
- * Returns the {@link Bundle} populated by this builder.
+ * Returns the {@link GenericDocumentParcel} holding the values for this
+ * {@link GenericDocument}.
*
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public Bundle getBundle() {
- return mBundle;
+ public GenericDocumentParcel getDocumentParcel() {
+ return mDocumentParcel;
}
/** Returns the unique identifier of the {@link GenericDocument}. */
@NonNull
public String getId() {
- return mId;
+ return mDocumentParcel.getId();
}
/** Returns the namespace of the {@link GenericDocument}. */
@NonNull
public String getNamespace() {
- return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
+ return mDocumentParcel.getNamespace();
}
/** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
@NonNull
public String getSchemaType() {
- return mSchemaType;
+ return mDocumentParcel.getSchemaType();
}
/**
@@ -224,7 +190,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Nullable
public List<String> getParentTypes() {
- List<String> result = mBundle.getStringArrayList(PARENT_TYPES_FIELD);
+ List<String> result = mDocumentParcel.getParentTypes();
if (result == null) {
return null;
}
@@ -238,7 +204,7 @@
*/
/*@exportToFramework:CurrentTimeMillisLong*/
public long getCreationTimestampMillis() {
- return mCreationTimestampMillis;
+ return mDocumentParcel.getCreationTimestampMillis();
}
/**
@@ -252,7 +218,7 @@
* until the app is uninstalled or {@link AppSearchSession#removeAsync} is called.
*/
public long getTtlMillis() {
- return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+ return mDocumentParcel.getTtlMillis();
}
/**
@@ -267,13 +233,13 @@
* <p>Any non-negative integer can be used a score.
*/
public int getScore() {
- return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
+ return mDocumentParcel.getScore();
}
/** Returns the names of all properties defined in this document. */
@NonNull
public Set<String> getPropertyNames() {
- return Collections.unmodifiableSet(mProperties.keySet());
+ return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames());
}
/**
@@ -334,61 +300,36 @@
*
* @param path The path to look for.
* @return The entry with the given path as an object or {@code null} if there is no such path.
- * The returned object will be one of the following types: {@code String[]}, {@code long[]},
- * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
+ * The returned object will be one of the following types: {@code String[]}, {@code long[]},
+ * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
*/
@Nullable
public Object getProperty(@NonNull String path) {
- Preconditions.checkNotNull(path);
+ Objects.requireNonNull(path);
Object rawValue =
- getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/0, mBundle);
+ getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0,
+ mDocumentParcel.getPropertyMap());
// Unpack the raw value into the types the user expects, if required.
- if (rawValue instanceof Bundle) {
- // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance
- // optimization for lookups.
- GenericDocument document = new GenericDocument((Bundle) rawValue);
+ if (rawValue instanceof GenericDocumentParcel) {
+ // getRawPropertyFromRawDocument may return a document as a bare documentParcel
+ // as a performance optimization for lookups.
+ GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue);
return new GenericDocument[]{document};
}
- if (rawValue instanceof List) {
- // byte[][] fields are packed into List<Bundle> where each Bundle contains just a single
- // entry: BYTE_ARRAY_FIELD -> byte[].
- @SuppressWarnings("unchecked")
- List<Bundle> bundles = (List<Bundle>) rawValue;
- byte[][] bytes = new byte[bundles.size()][];
- for (int i = 0; i < bundles.size(); i++) {
- Bundle bundle = bundles.get(i);
- if (bundle == null) {
- Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
- continue;
- }
- byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
- if (innerBytes == null) {
- Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
- continue;
- }
- bytes[i] = innerBytes;
- }
- return bytes;
- }
-
- if (rawValue instanceof Parcelable[]) {
- // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.
+ if (rawValue instanceof GenericDocumentParcel[]) {
+ // The underlying parcelable of nested GenericDocuments is packed into
+ // a Parcelable array.
// We must unpack it into GenericDocument instances.
- Parcelable[] bundles = (Parcelable[]) rawValue;
- GenericDocument[] documents = new GenericDocument[bundles.length];
- for (int i = 0; i < bundles.length; i++) {
- if (bundles[i] == null) {
- Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+ GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue;
+ GenericDocument[] documents = new GenericDocument[docParcels.length];
+ for (int i = 0; i < docParcels.length; i++) {
+ if (docParcels[i] == null) {
+ Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path);
continue;
}
- if (!(bundles[i] instanceof Bundle)) {
- Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass()
- + ", not a Bundle for path: " + path);
- continue;
- }
- documents[i] = new GenericDocument((Bundle) bundles[i]);
+ documents[i] = new GenericDocument(docParcels[i]);
}
return documents;
}
@@ -407,25 +348,21 @@
* But in the case where we collect documents across repeated nested documents, we need to
* recurse back into this method, and so we also keep track of the index into the path.
*
- * @param path the PropertyPath object representing the path
- * @param pathIndex the index into the path we start at
- * @param documentBundle the bundle that contains the path we are looking up
+ * @param path the PropertyPath object representing the path
+ * @param pathIndex the index into the path we start at
+ * @param propertyMap the map containing the path we are looking up
* @return the raw property
*/
@Nullable
@SuppressWarnings("deprecation")
private static Object getRawPropertyFromRawDocument(
- @NonNull PropertyPath path, int pathIndex, @NonNull Bundle documentBundle) {
- Preconditions.checkNotNull(path);
- Preconditions.checkNotNull(documentBundle);
- Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD));
-
-
+ @NonNull PropertyPath path, int pathIndex,
+ @NonNull Map<String, PropertyParcel> propertyMap) {
+ Objects.requireNonNull(path);
+ Objects.requireNonNull(propertyMap);
for (int i = pathIndex; i < path.size(); i++) {
- PathSegment segment = path.get(i);
-
- Object currentElementValue = properties.get(segment.getPropertyName());
-
+ PropertyPath.PathSegment segment = path.get(i);
+ Object currentElementValue = propertyMap.get(segment.getPropertyName());
if (currentElementValue == null) {
return null;
}
@@ -435,61 +372,77 @@
// "recipients[0]", currentElementValue now contains the value of "recipients" while we
// need the value of "recipients[0]".
int index = segment.getPropertyIndex();
- if (index != PathSegment.NON_REPEATED_CARDINALITY) {
+ if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) {
+ // For properties bundle, now we will only get PropertyParcel as the value.
+ PropertyParcel propertyParcel = (PropertyParcel) currentElementValue;
+
// Extract the right array element
Object extractedValue = null;
- if (currentElementValue instanceof String[]) {
- String[] stringValues = (String[]) currentElementValue;
- if (index < stringValues.length) {
+ if (propertyParcel.getStringValues() != null) {
+ String[] stringValues = propertyParcel.getStringValues();
+ if (stringValues != null && index < stringValues.length) {
extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
}
- } else if (currentElementValue instanceof long[]) {
- long[] longValues = (long[]) currentElementValue;
- if (index < longValues.length) {
+ } else if (propertyParcel.getLongValues() != null) {
+ long[] longValues = propertyParcel.getLongValues();
+ if (longValues != null && index < longValues.length) {
extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
}
- } else if (currentElementValue instanceof double[]) {
- double[] doubleValues = (double[]) currentElementValue;
- if (index < doubleValues.length) {
+ } else if (propertyParcel.getDoubleValues() != null) {
+ double[] doubleValues = propertyParcel.getDoubleValues();
+ if (doubleValues != null && index < doubleValues.length) {
extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
}
- } else if (currentElementValue instanceof boolean[]) {
- boolean[] booleanValues = (boolean[]) currentElementValue;
- if (index < booleanValues.length) {
+ } else if (propertyParcel.getBooleanValues() != null) {
+ boolean[] booleanValues = propertyParcel.getBooleanValues();
+ if (booleanValues != null && index < booleanValues.length) {
extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
}
- } else if (currentElementValue instanceof List) {
- @SuppressWarnings("unchecked")
- List<Bundle> bundles = (List<Bundle>) currentElementValue;
- if (index < bundles.size()) {
- extractedValue = bundles.subList(index, index + 1);
+ } else if (propertyParcel.getBytesValues() != null) {
+ byte[][] bytesValues = propertyParcel.getBytesValues();
+ if (bytesValues != null && index < bytesValues.length) {
+ extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1);
}
- } else if (currentElementValue instanceof Parcelable[]) {
+ } else if (propertyParcel.getDocumentValues() != null) {
// Special optimization: to avoid creating new singleton arrays for traversing
- // paths we return the bare document Bundle in this particular case.
- Parcelable[] bundles = (Parcelable[]) currentElementValue;
- if (index < bundles.length) {
- extractedValue = bundles[index];
+ // paths we return the bare document parcel in this particular case.
+ GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();
+ if (docValues != null && index < docValues.length) {
+ extractedValue = docValues[index];
+ }
+ } else if (propertyParcel.getEmbeddingValues() != null) {
+ EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues();
+ if (embeddingValues != null && index < embeddingValues.length) {
+ extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1);
}
} else {
- throw new IllegalStateException("Unsupported value type: "
- + currentElementValue);
+ throw new IllegalStateException(
+ "Unsupported value type: " + currentElementValue);
}
currentElementValue = extractedValue;
}
// at the end of the path, either something like "...foo" or "...foo[1]"
if (currentElementValue == null || i == path.size() - 1) {
+ if (currentElementValue != null && currentElementValue instanceof PropertyParcel) {
+ // Unlike previous bundle-based implementation, now each
+ // value is wrapped in PropertyParcel.
+ // Here we need to get and return the actual value for non-repeated fields.
+ currentElementValue = ((PropertyParcel) currentElementValue).getValues();
+ }
return currentElementValue;
}
- // currentElementValue is now a Bundle or Parcelable[], we can continue down the path
- if (currentElementValue instanceof Bundle) {
- properties = ((Bundle) currentElementValue).getBundle(PROPERTIES_FIELD);
- } else if (currentElementValue instanceof Parcelable[]) {
- Parcelable[] parcelables = (Parcelable[]) currentElementValue;
- if (parcelables.length == 1) {
- properties = ((Bundle) parcelables[0]).getBundle(PROPERTIES_FIELD);
+ // currentElementValue is now a GenericDocumentParcel or PropertyParcel,
+ // we can continue down the path.
+ if (currentElementValue instanceof GenericDocumentParcel) {
+ propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap();
+ } else if (currentElementValue instanceof PropertyParcel
+ && ((PropertyParcel) currentElementValue).getDocumentValues() != null) {
+ GenericDocumentParcel[] docParcels =
+ ((PropertyParcel) currentElementValue).getDocumentValues();
+ if (docParcels != null && docParcels.length == 1) {
+ propertyMap = docParcels[0].getPropertyMap();
continue;
}
@@ -516,17 +469,21 @@
// repeated values. The implementation is optimized for these two cases, requiring
// no additional allocations. So we've decided that the above performance
// characteristics are OK for the less used path.
- List<Object> accumulator = new ArrayList<>(parcelables.length);
- for (Parcelable parcelable : parcelables) {
- // recurse as we need to branch
- Object value = getRawPropertyFromRawDocument(path, /*pathIndex=*/i + 1,
- (Bundle) parcelable);
- if (value != null) {
- accumulator.add(value);
+ if (docParcels != null) {
+ List<Object> accumulator = new ArrayList<>(docParcels.length);
+ for (GenericDocumentParcel docParcel : docParcels) {
+ // recurse as we need to branch
+ Object value =
+ getRawPropertyFromRawDocument(
+ path, /*pathIndex=*/ i + 1,
+ ((GenericDocumentParcel) docParcel).getPropertyMap());
+ if (value != null) {
+ accumulator.add(value);
+ }
}
+ // Break the path traversing loop
+ return flattenAccumulator(accumulator);
}
- // Break the path traversing loop
- return flattenAccumulator(accumulator);
} else {
Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
return null;
@@ -540,10 +497,10 @@
* Combines accumulated repeated properties from multiple documents into a single array.
*
* @param accumulator List containing objects of the following types: {@code String[]},
- * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List<Bundle>},
- * or {@code Parcelable[]}.
+ * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]},
+ * or {@code GenericDocumentParcelable[]}.
* @return The result of concatenating each individual list element into a larger array/list of
- * the same type.
+ * the same type.
*/
@Nullable
private static Object flattenAccumulator(@NonNull List<Object> accumulator) {
@@ -607,28 +564,29 @@
}
return result;
}
- if (first instanceof List) {
+ if (first instanceof byte[][]) {
int length = 0;
for (int i = 0; i < accumulator.size(); i++) {
- length += ((List<?>) accumulator.get(i)).size();
+ length += ((byte[][]) accumulator.get(i)).length;
}
- List<Bundle> result = new ArrayList<>(length);
+ byte[][] result = new byte[length][];
+ int total = 0;
for (int i = 0; i < accumulator.size(); i++) {
- @SuppressWarnings("unchecked")
- List<Bundle> castValue = (List<Bundle>) accumulator.get(i);
- result.addAll(castValue);
+ byte[][] castValue = (byte[][]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
}
return result;
}
- if (first instanceof Parcelable[]) {
+ if (first instanceof GenericDocumentParcel[]) {
int length = 0;
for (int i = 0; i < accumulator.size(); i++) {
- length += ((Parcelable[]) accumulator.get(i)).length;
+ length += ((GenericDocumentParcel[]) accumulator.get(i)).length;
}
- Parcelable[] result = new Parcelable[length];
+ GenericDocumentParcel[] result = new GenericDocumentParcel[length];
int total = 0;
for (int i = 0; i < accumulator.size(); i++) {
- Parcelable[] castValue = (Parcelable[]) accumulator.get(i);
+ GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i);
System.arraycopy(castValue, 0, result, total, castValue.length);
total += castValue.length;
}
@@ -754,6 +712,27 @@
return propertyArray[0];
}
+ /**
+ * Retrieves an {@code EmbeddingVector} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@code EmbeddingVector[]} associated with the given path or
+ * {@code null} if there is no such value or the value is of a different type.
+ */
+ @Nullable
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public EmbeddingVector getPropertyEmbedding(@NonNull String path) {
+ Preconditions.checkNotNull(path);
+ EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
/** Prints a warning to logcat if the given propertyLength is greater than 1. */
private static void warnIfSinglePropertyTooLong(
@NonNull String propertyType, @NonNull String path, int propertyLength) {
@@ -862,13 +841,13 @@
* returns {@code null}.
*
* <!--@exportToFramework:ifJetpack()-->
- * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
- * this method returns an empty {@code byte[][]}.
+ * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+ * this method returns an empty {@code byte[][]}.
* <!--@exportToFramework:else()
- * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
- * this method returns an empty {@code byte[][]} starting in
- * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
- * versions of Android.
+ * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+ * this method returns an empty {@code byte[][]} starting in
+ * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
+ * versions of Android.
* -->
*
* @param path The path to look for.
@@ -892,13 +871,13 @@
* returns {@code null}.
*
* <!--@exportToFramework:ifJetpack()-->
- * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
- * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
+ * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+ * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
* <!--@exportToFramework:else()
- * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
- * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
- * in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
- * versions of Android.
+ * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+ * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
+ * in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
+ * versions of Android.
* -->
*
* @param path The path to look for.
@@ -914,11 +893,36 @@
}
/**
+ * Retrieves a repeated {@code EmbeddingVector[]} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method
+ * returns {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty
+ * {@code EmbeddingVector[]}, this method returns an empty
+ * {@code EmbeddingVector[]}.
+ *
+ * @param path The path to look for.
+ * @return The {@code EmbeddingVector[]} associated with the given path, or
+ * {@code null} if no value is set or the value is of a different type.
+ */
+ @SuppressLint({"ArrayReturn", "NullableCollection"})
+ @Nullable
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) {
+ Preconditions.checkNotNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, EmbeddingVector[].class);
+ }
+
+ /**
* Casts a repeated property to the provided type, logging an error and returning {@code null}
* if the cast fails.
*
- * @param path Path to the property within the document. Used for logging.
- * @param value Value of the property
+ * @param path Path to the property within the document. Used for logging.
+ * @param value Value of the property
* @param tClass Class to cast the value into
*/
@Nullable
@@ -1056,6 +1060,7 @@
* {@link GenericDocument.Builder}.
*
* <p>The returned builder is a deep copy whose data is separate from this document.
+ *
* @deprecated This API is not compliant with API guidelines.
* Use {@link Builder#Builder(GenericDocument)} instead.
* <!--@exportToFramework:hide-->
@@ -1064,8 +1069,7 @@
@NonNull
@Deprecated
public GenericDocument.Builder<GenericDocument.Builder<?>> toBuilder() {
- Bundle clonedBundle = BundleUtil.deepCopy(mBundle);
- return new GenericDocument.Builder<>(clonedBundle);
+ return new Builder<>(new GenericDocumentParcel.Builder(mDocumentParcel));
}
@Override
@@ -1077,15 +1081,12 @@
return false;
}
GenericDocument otherDocument = (GenericDocument) other;
- return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
+ return mDocumentParcel.equals(otherDocument.mDocumentParcel);
}
@Override
public int hashCode() {
- if (mHashCode == null) {
- mHashCode = BundleUtil.deepHashCode(mBundle);
- }
- return mHashCode;
+ return mDocumentParcel.hashCode();
}
@Override
@@ -1099,7 +1100,7 @@
/**
* Appends a debug string for the {@link GenericDocument} instance to the given string builder.
*
- * @param builder the builder to append to.
+ * @param builder the builder to append to.
*/
void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) {
Preconditions.checkNotNull(builder);
@@ -1147,9 +1148,9 @@
/**
* Appends a debug string for the given document property to the given string builder.
*
- * @param propertyName name of property to create string for.
- * @param property property object to create string for.
- * @param builder the builder to append to.
+ * @param propertyName name of property to create string for.
+ * @param property property object to create string for.
+ * @param builder the builder to append to.
*/
private void appendPropertyString(@NonNull String propertyName, @NonNull Object property,
@NonNull IndentingStringBuilder builder) {
@@ -1178,7 +1179,7 @@
builder.append("\"").append((String) propertyElement).append("\"");
} else if (propertyElement instanceof byte[]) {
builder.append(Arrays.toString((byte[]) propertyElement));
- } else {
+ } else if (propertyElement != null) {
builder.append(propertyElement.toString());
}
if (i != propertyArrLength - 1) {
@@ -1197,11 +1198,10 @@
// This builder is specifically designed to be extended by classes deriving from
// GenericDocument.
@SuppressLint("StaticFinalBuilder")
+ @SuppressWarnings("rawtypes")
public static class Builder<BuilderType extends Builder> {
- private Bundle mBundle;
- private Bundle mProperties;
+ private final GenericDocumentParcel.Builder mDocumentParcelBuilder;
private final BuilderType mBuilderTypeInstance;
- private boolean mBuilt = false;
/**
* Creates a new {@link GenericDocument.Builder}.
@@ -1228,41 +1228,31 @@
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(schemaType);
- mBundle = new Bundle();
mBuilderTypeInstance = (BuilderType) this;
- mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
- mBundle.putString(GenericDocument.ID_FIELD, id);
- mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
- mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
- mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);
-
- mProperties = new Bundle();
- mBundle.putBundle(PROPERTIES_FIELD, mProperties);
+ mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType);
}
/**
- * Creates a new {@link GenericDocument.Builder} from the given Bundle.
+ * Creates a new {@link GenericDocument.Builder} from the given
+ * {@link GenericDocumentParcel.Builder}.
*
* <p>The bundle is NOT copied.
*/
@SuppressWarnings("unchecked")
- Builder(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- // mProperties is NonNull and initialized to empty Bundle() in builder.
- mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));
+ Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) {
+ mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder);
mBuilderTypeInstance = (BuilderType) this;
}
/**
* Creates a new {@link GenericDocument.Builder} from the given GenericDocument.
*
- * <p>The GenericDocument is deep copied, i.e. changes to the new GenericDocument
- * returned by this function will NOT affect the original GenericDocument.
- * <!--@exportToFramework:hide-->
+ * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument
+ * returned by this function and will NOT affect the original GenericDocument.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR)
public Builder(@NonNull GenericDocument document) {
- this(BundleUtil.deepCopy(document.getBundle()));
+ this(new GenericDocumentParcel.Builder(document.mDocumentParcel));
}
/**
@@ -1272,14 +1262,13 @@
* <p>Document IDs are unique within a namespace.
*
* <p>The number of namespaces per app should be kept small for efficiency reasons.
- * <!--@exportToFramework:hide-->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
@CanIgnoreReturnValue
@NonNull
public BuilderType setNamespace(@NonNull String namespace) {
Preconditions.checkNotNull(namespace);
- resetIfBuilt();
- mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+ mDocumentParcelBuilder.setNamespace(namespace);
return mBuilderTypeInstance;
}
@@ -1287,15 +1276,17 @@
* Sets the ID of this document, changing the value provided in the constructor. No
* special values are reserved or understood by the infrastructure.
*
- * <p>Document IDs are unique within a namespace.
- * <!--@exportToFramework:hide-->
+ * <p>Document IDs are unique within the combination of package, database, and namespace.
+ *
+ * <p>Setting a document with a duplicate id will overwrite the original document with
+ * the new document, enforcing uniqueness within the above constraint.
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
@CanIgnoreReturnValue
@NonNull
public BuilderType setId(@NonNull String id) {
Preconditions.checkNotNull(id);
- resetIfBuilt();
- mBundle.putString(GenericDocument.ID_FIELD, id);
+ mDocumentParcelBuilder.setId(id);
return mBuilderTypeInstance;
}
@@ -1303,15 +1294,15 @@
* 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#setSchemaAsync}.
- * <!--@exportToFramework:hide-->
+ * {@link AppSearchSchema} object previously provided to
+ * {@link AppSearchSession#setSchemaAsync}.
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
@CanIgnoreReturnValue
@NonNull
public BuilderType setSchemaType(@NonNull String schemaType) {
Preconditions.checkNotNull(schemaType);
- resetIfBuilt();
- mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+ mDocumentParcelBuilder.setSchemaType(schemaType);
return mBuilderTypeInstance;
}
@@ -1326,9 +1317,7 @@
@NonNull
public BuilderType setParentTypes(@NonNull List<String> parentTypes) {
Preconditions.checkNotNull(parentTypes);
- resetIfBuilt();
- mBundle.putStringArrayList(GenericDocument.PARENT_TYPES_FIELD,
- new ArrayList<>(parentTypes));
+ mDocumentParcelBuilder.setParentTypes(parentTypes);
return mBuilderTypeInstance;
}
@@ -1352,8 +1341,7 @@
if (score < 0) {
throw new IllegalArgumentException("Document score cannot be negative.");
}
- resetIfBuilt();
- mBundle.putInt(GenericDocument.SCORE_FIELD, score);
+ mDocumentParcelBuilder.setScore(score);
return mBuilderTypeInstance;
}
@@ -1371,9 +1359,7 @@
@NonNull
public BuilderType setCreationTimestampMillis(
/*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
- resetIfBuilt();
- mBundle.putLong(
- GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis);
+ mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis);
return mBuilderTypeInstance;
}
@@ -1397,8 +1383,7 @@
if (ttlMillis < 0) {
throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
}
- resetIfBuilt();
- mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
+ mDocumentParcelBuilder.setTtlMillis(ttlMillis);
return mBuilderTypeInstance;
}
@@ -1406,9 +1391,9 @@
* Sets one or multiple {@code String} values for a property, replacing its previous
* values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@code String} values of the property.
* @throws IllegalArgumentException if no values are provided, or if a passed in
* {@code String} is {@code null} or "".
@@ -1418,8 +1403,13 @@
public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The String at " + i + " is null.");
+ }
+ }
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
@@ -1427,9 +1417,9 @@
* Sets one or multiple {@code boolean} values for a property, replacing its previous
* values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@code boolean} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
@@ -1438,8 +1428,8 @@
public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
@@ -1447,9 +1437,9 @@
* Sets one or multiple {@code long} values for a property, replacing its previous
* values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@code long} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
@@ -1458,8 +1448,8 @@
public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
@@ -1467,9 +1457,9 @@
* Sets one or multiple {@code double} values for a property, replacing its previous
* values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@code double} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
@@ -1478,17 +1468,17 @@
public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code byte[]} for a property, replacing its previous values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@code byte[]} of the property.
* @throws IllegalArgumentException if no values are provided, or if a passed in
* {@code byte[]} is {@code null}, or if name is empty.
@@ -1498,8 +1488,13 @@
public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The byte[] at " + i + " is null.");
+ }
+ }
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
@@ -1507,9 +1502,9 @@
* Sets one or multiple {@link GenericDocument} values for a property, replacing its
* previous values.
*
- * @param name the name associated with the {@code values}. Must match the name
- * for this property as given in
- * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
* @param values the {@link GenericDocument} values of the property.
* @throws IllegalArgumentException if no values are provided, or if a passed in
* {@link GenericDocument} is {@code null}, or if name
@@ -1521,8 +1516,43 @@
@NonNull String name, @NonNull GenericDocument... values) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(values);
- resetIfBuilt();
- putInPropertyBundle(name, values);
+ validatePropertyName(name);
+ GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length];
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The document at " + i + " is null.");
+ }
+ documentParcels[i] = values[i].getDocumentParcel();
+ }
+ mDocumentParcelBuilder.putInPropertyMap(name, documentParcels);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code EmbeddingVector} values for a property, replacing
+ * its previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name
+ * for this property as given in
+ * {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code EmbeddingVector} values of the property.
+ * @throws IllegalArgumentException if the name is empty or {@code null}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public BuilderType setPropertyEmbedding(@NonNull String name,
+ @NonNull EmbeddingVector... values) {
+ Preconditions.checkNotNull(name);
+ Preconditions.checkNotNull(values);
+ validatePropertyName(name);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException(
+ "The EmbeddingVector at " + i + " is null.");
+ }
+ }
+ mDocumentParcelBuilder.putInPropertyMap(name, values);
return mBuilderTypeInstance;
}
@@ -1531,95 +1561,27 @@
*
* <p>Note that this method does not support property paths.
*
+ * <p>You should check for the existence of the property in {@link #getPropertyNames} if
+ * you need to make sure the property being cleared actually exists.
+ *
+ * <p>If the string passed is an invalid or nonexistent property, no error message or
+ * behavior will be observed.
+ *
* @param name The name of the property to clear.
- * <!--@exportToFramework:hide-->
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
@CanIgnoreReturnValue
@NonNull
public BuilderType clearProperty(@NonNull String name) {
Preconditions.checkNotNull(name);
- resetIfBuilt();
- mProperties.remove(name);
+ mDocumentParcelBuilder.clearProperty(name);
return mBuilderTypeInstance;
}
- private void putInPropertyBundle(@NonNull String name, @NonNull String[] values)
- throws IllegalArgumentException {
- validatePropertyName(name);
- for (int i = 0; i < values.length; i++) {
- if (values[i] == null) {
- throw new IllegalArgumentException("The String at " + i + " is null.");
- }
- }
- mProperties.putStringArray(name, values);
- }
-
- private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {
- validatePropertyName(name);
- mProperties.putBooleanArray(name, values);
- }
-
- private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {
- validatePropertyName(name);
- mProperties.putDoubleArray(name, values);
- }
-
- private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {
- validatePropertyName(name);
- mProperties.putLongArray(name, values);
- }
-
- /**
- * Converts and saves a byte[][] into {@link #mProperties}.
- *
- * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
- * into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
- */
- private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {
- validatePropertyName(name);
- ArrayList<Bundle> bundles = new ArrayList<>(values.length);
- for (int i = 0; i < values.length; i++) {
- if (values[i] == null) {
- throw new IllegalArgumentException("The byte[] at " + i + " is null.");
- }
- Bundle bundle = new Bundle();
- bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
- bundles.add(bundle);
- }
- mProperties.putParcelableArrayList(name, bundles);
- }
-
- private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {
- validatePropertyName(name);
- Parcelable[] documentBundles = new Parcelable[values.length];
- for (int i = 0; i < values.length; i++) {
- if (values[i] == null) {
- throw new IllegalArgumentException("The document at " + i + " is null.");
- }
- documentBundles[i] = values[i].mBundle;
- }
- mProperties.putParcelableArray(name, documentBundles);
- }
-
/** Builds the {@link GenericDocument} object. */
@NonNull
public GenericDocument build() {
- mBuilt = true;
- // Set current timestamp for creation timestamp by default.
- if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) {
- mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
- System.currentTimeMillis());
- }
- return new GenericDocument(mBundle);
- }
-
- private void resetIfBuilt() {
- if (mBuilt) {
- mBundle = BundleUtil.deepCopy(mBundle);
- // mProperties is NonNull and initialized to empty Bundle() in builder.
- mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));
- mBuilt = false;
- }
+ return new GenericDocument(mDocumentParcelBuilder.build());
}
/** Method to ensure property names are not blank */
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 ee59e34..be17430 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -16,9 +16,20 @@
package androidx.appsearch.app;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetByDocumentIdRequestCreator;
+import androidx.appsearch.util.BundleUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -29,6 +40,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -37,7 +49,13 @@
*
* @see AppSearchSession#getByDocumentIdAsync
*/
-public final class GetByDocumentIdRequest {
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "GetByDocumentIdRequestCreator")
+public final class GetByDocumentIdRequest extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<GetByDocumentIdRequest> CREATOR =
+ new GetByDocumentIdRequestCreator();
/**
* Schema type to be used in
* {@link GetByDocumentIdRequest.Builder#addProjection}
@@ -45,15 +63,30 @@
* property paths set.
*/
public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
- private final String mNamespace;
- private final Set<String> mIds;
- private final Map<String, List<String>> mTypePropertyPathsMap;
- GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set<String> ids, @NonNull Map<String,
- List<String>> typePropertyPathsMap) {
- mNamespace = Preconditions.checkNotNull(namespace);
- mIds = Preconditions.checkNotNull(ids);
- mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
+ @NonNull
+ @Field(id = 1, getter = "getNamespace")
+ private final String mNamespace;
+ @NonNull
+ @Field(id = 2)
+ final List<String> mIds;
+ @NonNull
+ @Field(id = 3)
+ final Bundle mTypePropertyPaths;
+
+ /**
+ * Cache of the ids. Comes from inflating mIds at first use.
+ */
+ @Nullable private Set<String> mIdsCached;
+
+ @Constructor
+ GetByDocumentIdRequest(
+ @Param(id = 1) @NonNull String namespace,
+ @Param(id = 2) @NonNull List<String> ids,
+ @Param(id = 3) @NonNull Bundle typePropertyPaths) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mIds = Objects.requireNonNull(ids);
+ mTypePropertyPaths = Objects.requireNonNull(typePropertyPaths);
}
/** Returns the namespace attached to the request. */
@@ -65,7 +98,10 @@
/** Returns the set of document IDs attached to the request. */
@NonNull
public Set<String> getIds() {
- return Collections.unmodifiableSet(mIds);
+ if (mIdsCached == null) {
+ mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+ }
+ return mIdsCached;
}
/**
@@ -78,11 +114,15 @@
*/
@NonNull
public Map<String, List<String>> getProjections() {
- Map<String, List<String>> copy = new ArrayMap<>();
- for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
- copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ Set<String> schemas = mTypePropertyPaths.keySet();
+ Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+ for (String schema : schemas) {
+ List<String> propertyPaths = mTypePropertyPaths.getStringArrayList(schema);
+ if (propertyPaths != null) {
+ typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPaths));
+ }
}
- return copy;
+ return typePropertyPathsMap;
}
/**
@@ -95,38 +135,34 @@
*/
@NonNull
public Map<String, List<PropertyPath>> getProjectionPaths() {
- Map<String, List<PropertyPath>> copy = new ArrayMap<>(mTypePropertyPathsMap.size());
- for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
- List<PropertyPath> propertyPathList = new ArrayList<>(entry.getValue().size());
- for (String p: entry.getValue()) {
- propertyPathList.add(new PropertyPath(p));
+ Set<String> schemas = mTypePropertyPaths.keySet();
+ Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+ for (String schema : schemas) {
+ List<String> paths = mTypePropertyPaths.getStringArrayList(schema);
+ if (paths != null) {
+ int pathsSize = paths.size();
+ List<PropertyPath> propertyPathList = new ArrayList<>(pathsSize);
+ for (int i = 0; i < pathsSize; i++) {
+ propertyPathList.add(new PropertyPath(paths.get(i)));
+ }
+ typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPathList));
}
- copy.put(entry.getKey(), propertyPathList);
}
- return copy;
+ return typePropertyPathsMap;
}
- /**
- * Returns a map from schema type to property paths to be used for projection.
- *
- * <p>If the map is empty, then all properties will be retrieved for all results.
- *
- * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map.
- * This is not meant to be unhidden and should only be used by internal classes.
- *
- * @exportToFramework:hide
- */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Map<String, List<String>> getProjectionsInternal() {
- return mTypePropertyPathsMap;
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ GetByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link GetByDocumentIdRequest} objects. */
public static final class Builder {
private final String mNamespace;
- private ArraySet<String> mIds = new ArraySet<>();
- private ArrayMap<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+ private List<String> mIds = new ArrayList<>();
+ private Bundle mProjectionTypePropertyPaths = new Bundle();
private boolean mBuilt = false;
/** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
@@ -176,12 +212,12 @@
Preconditions.checkNotNull(schemaType);
Preconditions.checkNotNull(propertyPaths);
resetIfBuilt();
- List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+ ArrayList<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
for (String propertyPath : propertyPaths) {
Preconditions.checkNotNull(propertyPath);
propertyPathsList.add(propertyPath);
}
- mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+ mProjectionTypePropertyPaths.putStringArrayList(schemaType, propertyPathsList);
return this;
}
@@ -223,11 +259,11 @@
private void resetIfBuilt() {
if (mBuilt) {
- mIds = new ArraySet<>(mIds);
+ mIds = new ArrayList<>(mIds);
// No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since
// the builder only replaces it, never adds to it. So even if the builder is used
// again, the previous one will remain with the object.
- mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);
+ mProjectionTypePropertyPaths = BundleUtil.deepCopy(mProjectionTypePropertyPaths);
mBuilt = false;
}
}
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 0933654..74f370e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -17,7 +17,8 @@
package androidx.appsearch.app;
import android.annotation.SuppressLint;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
@@ -25,6 +26,11 @@
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetSchemaResponseCreator;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -36,58 +42,88 @@
import java.util.Set;
/** 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";
[email protected](creator = "GetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class GetSchemaResponse extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<GetSchemaResponse> CREATOR =
+ new GetSchemaResponseCreator();
+
+ @Field(id = 1, getter = "getVersion")
+ private final int mVersion;
+
+ @Field(id = 2)
+ final List<AppSearchSchema> mSchemas;
+
+ /**
+ * List of VisibilityConfigs for the current schema. May be {@code null} if retrieving the
+ * visibility settings is not possible on the current backend.
+ */
+ @Field(id = 3)
+ @Nullable
+ final List<InternalVisibilityConfig> mVisibilityConfigs;
+
+ /**
+ * This set contains all schemas most recently successfully provided to
+ * {@link AppSearchSession#setSchemaAsync}. We do lazy fetch, the object will be created when
+ * you first time fetch it.
+ */
+ @Nullable
+ private Set<AppSearchSchema> mSchemasCached;
+
/**
* 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 you first time fetch it.
*/
@Nullable
- private Set<String> mSchemasNotDisplayedBySystem;
+ private Set<String> mSchemasNotDisplayedBySystemCached;
+
/**
* 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 you first time fetch it.
*/
@Nullable
- private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+ private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackagesCached;
/**
* 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 you 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
+ * objects under the {@code schemaType} if they hold ALL required permissions of ANY
* combinations.
- * The value set represents
- * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
*/
@Nullable
- private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
-
- private final Bundle mBundle;
-
- GetSchemaResponse(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
+ private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissionsCached;
/**
- * Returns the {@link Bundle} populated by this builder.
- * @exportToFramework:hide
+ * This map contains all publicly visible schemas and the {@link PackageIdentifier} specifying
+ * the package that the schemas are from.
*/
- @NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Bundle getBundle() {
- return mBundle;
+ @Nullable
+ private Map<String, PackageIdentifier> mPubliclyVisibleSchemasCached;
+
+ /**
+ * This map contains all {@link SchemaVisibilityConfig}s 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 you first time fetch it.
+ */
+ @Nullable
+ private Map<String, Set<SchemaVisibilityConfig>> mSchemasVisibleToConfigsCached;
+
+ @Constructor
+ GetSchemaResponse(
+ @Param(id = 1) int version,
+ @Param(id = 2) @NonNull List<AppSearchSchema> schemas,
+ @Param(id = 3) @Nullable List<InternalVisibilityConfig> visibilityConfigs) {
+ mVersion = version;
+ mSchemas = Preconditions.checkNotNull(schemas);
+ mVisibilityConfigs = visibilityConfigs;
}
/**
@@ -97,25 +133,19 @@
*/
@IntRange(from = 0)
public int getVersion() {
- return mBundle.getInt(VERSION_FIELD);
+ return mVersion;
}
/**
* Return the schemas most recently successfully provided to
* {@link AppSearchSession#setSchemaAsync}.
- *
- * <p>It is inefficient to call this method repeatedly.
*/
@NonNull
- @SuppressWarnings("deprecation")
public Set<AppSearchSchema> getSchemas() {
- ArrayList<Bundle> schemaBundles = Preconditions.checkNotNull(
- mBundle.getParcelableArrayList(SCHEMAS_FIELD));
- Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
- for (int i = 0; i < schemaBundles.size(); i++) {
- schemas.add(new AppSearchSchema(schemaBundles.get(i)));
+ if (mSchemasCached == null) {
+ mSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mSchemas));
}
- return schemas;
+ return mSchemasCached;
}
/**
@@ -126,21 +156,22 @@
* called with false.
* <!--@exportToFramework:else()-->
*/
- // @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));
+ List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+ if (mSchemasNotDisplayedBySystemCached == null) {
+ Set<String> copy = new ArraySet<>();
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ if (visibilityConfigs.get(i).isNotDisplayedBySystem()) {
+ copy.add(visibilityConfigs.get(i).getSchemaType());
+ }
+ }
+ mSchemasNotDisplayedBySystemCached = Collections.unmodifiableSet(copy);
}
- return mSchemasNotDisplayedBySystem;
+ return mSchemasNotDisplayedBySystemCached;
}
/**
@@ -151,37 +182,32 @@
* called with false.
* <!--@exportToFramework:else()-->
*/
- // @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 = Preconditions.checkNotNull(
- mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));
+ List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+ if (mSchemasVisibleToPackagesCached == null) {
Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
- for (String key : schemaVisibleToPackagesBundle.keySet()) {
- List<Bundle> PackageIdentifierBundles = Preconditions.checkNotNull(
- 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)));
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+ List<PackageIdentifier> visibleToPackages =
+ visibilityConfig.getVisibilityConfig().getAllowedPackages();
+ if (!visibleToPackages.isEmpty()) {
+ copy.put(
+ visibilityConfig.getSchemaType(),
+ Collections.unmodifiableSet(new ArraySet<>(visibleToPackages)));
}
- copy.put(key, packageIdentifiers);
}
- mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
+ mSchemasVisibleToPackagesCached = Collections.unmodifiableMap(copy);
}
- return mSchemasVisibleToPackages;
+ return mSchemasVisibleToPackagesCached;
}
/**
- * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
- * combinations that querier must hold to access that schema type.
+ * Returns a mapping of schema types to the set of {@link android.Manifest.permission}
+ * combination sets 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.
@@ -189,12 +215,12 @@
* <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>
+ * <li>A querier holding both PermissionA and PermissionB has access.</li>
+ * <li>A querier holding both PermissionC and PermissionD has access.</li>
+ * <li>A querier holding only PermissionE has access.</li>
+ * <li>A querier holding both PermissionA and PermissionE has access.</li>
+ * <li>A querier holding only PermissionA doesn't have access.</li>
+ * <li>A querier holding only PermissionA and PermissionC doesn't have access.</li>
* </ul>
*
* @return The map contains schema type and all combinations of required permission for querier
@@ -208,56 +234,118 @@
* called with false.
* <!--@exportToFramework:else()-->
*/
- // @exportToFramework:startStrip()
+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
- // @exportToFramework:endStrip()
@NonNull
- @SuppressWarnings("deprecation")
public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
- checkGetVisibilitySettingSupported();
- if (mSchemasVisibleToPermissions == null) {
+ List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+ if (mSchemasVisibleToPermissionsCached == null) {
Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
- Bundle schemaVisibleToPermissionBundle = Preconditions.checkNotNull(
- 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)));
- }
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+ Set<Set<Integer>> visibleToPermissions =
+ visibilityConfig.getVisibilityConfig().getRequiredPermissions();
+ if (!visibleToPermissions.isEmpty()) {
+ copy.put(
+ visibilityConfig.getSchemaType(),
+ Collections.unmodifiableSet(visibleToPermissions));
}
- copy.put(key, visibleToPermissions);
}
- mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
+ mSchemasVisibleToPermissionsCached = Collections.unmodifiableMap(copy);
}
- return mSchemasVisibleToPermissions;
+ return mSchemasVisibleToPermissionsCached;
}
- private void checkGetVisibilitySettingSupported() {
- if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
+ /**
+ * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying
+ * the package the schemas are from.
+ *
+ * <p> If no schemas have been set as publicly visible, an empty set will be returned.
+ * <!--@exportToFramework:ifJetpack()-->
+ * @throws UnsupportedOperationException if {@link Builder#setVisibilitySettingSupported} was
+ * called with false.
+ * <!--@exportToFramework:else()-->
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ @NonNull
+ public Map<String, PackageIdentifier> getPubliclyVisibleSchemas() {
+ List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+ if (mPubliclyVisibleSchemasCached == null) {
+ Map<String, PackageIdentifier> copy = new ArrayMap<>();
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+ PackageIdentifier publiclyVisibleTargetPackage =
+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();
+ if (publiclyVisibleTargetPackage != null) {
+ copy.put(visibilityConfig.getSchemaType(), publiclyVisibleTargetPackage);
+ }
+ }
+ mPubliclyVisibleSchemasCached = Collections.unmodifiableMap(copy);
+ }
+ return mPubliclyVisibleSchemasCached;
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
+ * access to that schema type.
+ *
+ * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+ @NonNull
+ public Map<String, Set<SchemaVisibilityConfig>> getSchemaTypesVisibleToConfigs() {
+ List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+ if (mSchemasVisibleToConfigsCached == null) {
+ Map<String, Set<SchemaVisibilityConfig>> copy = new ArrayMap<>();
+ for (int i = 0; i < visibilityConfigs.size(); i++) {
+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+ Set<SchemaVisibilityConfig> nestedVisibilityConfigs =
+ visibilityConfig.getVisibleToConfigs();
+ if (!nestedVisibilityConfigs.isEmpty()) {
+ copy.put(visibilityConfig.getSchemaType(),
+ Collections.unmodifiableSet(nestedVisibilityConfigs));
+ }
+ }
+ mSchemasVisibleToConfigsCached = Collections.unmodifiableMap(copy);
+ }
+ return mSchemasVisibleToConfigsCached;
+ }
+
+ @NonNull
+ private List<InternalVisibilityConfig> getVisibilityConfigsOrThrow() {
+ List<InternalVisibilityConfig> visibilityConfigs = mVisibilityConfigs;
+ if (visibilityConfigs == null) {
throw new UnsupportedOperationException("Get visibility setting is not supported with "
+ "this backend/Android API level combination.");
}
+ return visibilityConfigs;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ GetSchemaResponseCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link GetSchemaResponse} objects. */
public static final class Builder {
private int mVersion = 0;
- private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+ private ArrayList<AppSearchSchema> mSchemas = 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 Map<String, InternalVisibilityConfig.Builder> mVisibilityConfigBuilders;
private boolean mBuilt = false;
/** Create a {@link Builder} object} */
@@ -284,7 +372,7 @@
public Builder addSchema(@NonNull AppSearchSchema schema) {
Preconditions.checkNotNull(schema);
resetIfBuilt();
- mSchemaBundles.add(schema.getBundle());
+ mSchemas.add(schema);
return this;
}
@@ -302,10 +390,9 @@
public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
Preconditions.checkNotNull(schemaType);
resetIfBuilt();
- if (mSchemasNotDisplayedBySystem == null) {
- mSchemasNotDisplayedBySystem = new ArrayList<>();
- }
- mSchemasNotDisplayedBySystem.add(schemaType);
+ InternalVisibilityConfig.Builder visibilityConfigBuilder =
+ getOrCreateVisibilityConfigBuilder(schemaType);
+ visibilityConfigBuilder.setNotDisplayedBySystem(true);
return this;
}
@@ -337,11 +424,11 @@
Preconditions.checkNotNull(schemaType);
Preconditions.checkNotNull(packageIdentifiers);
resetIfBuilt();
- ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
+ InternalVisibilityConfig.Builder visibilityConfigBuilder =
+ getOrCreateVisibilityConfigBuilder(schemaType);
for (PackageIdentifier packageIdentifier : packageIdentifiers) {
- bundles.add(packageIdentifier.getBundle());
+ visibilityConfigBuilder.addVisibleToPackage(packageIdentifier);
}
- mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
return this;
}
@@ -364,41 +451,103 @@
* <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
* </ul>
*
+ * @param schemaType The schema type to set visibility on.
+ * @param visibleToPermissionSets The Sets of Android permissions that will be required to
+ * access the given schema.
* @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.
*/
+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
// Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
+ // @SetSchemaRequest is an IntDef annotation applied to Set<Set<Integer>>.
+ @SuppressWarnings("SupportAnnotationUsage")
@NonNull
public Builder setRequiredPermissionsForSchemaTypeVisibility(
@NonNull String schemaType,
@SetSchemaRequest.AppSearchSupportedPermission @NonNull
- Set<Set<Integer>> visibleToPermissions) {
+ Set<Set<Integer>> visibleToPermissionSets) {
Preconditions.checkNotNull(schemaType);
- Preconditions.checkNotNull(visibleToPermissions);
+ Preconditions.checkNotNull(visibleToPermissionSets);
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);
+ InternalVisibilityConfig.Builder visibilityConfigBuilder =
+ getOrCreateVisibilityConfigBuilder(schemaType);
+ for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+ visibilityConfigBuilder.addVisibleToPermissions(visibleToPermissions);
}
- mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,
- visibleToPermissionsBundle);
+ return this;
+ }
+
+ /**
+ * Specify that the schema should be publicly available, to packages which already have
+ * visibility to {@code packageIdentifier}.
+ *
+ * @param schemaType the schema to make publicly accessible.
+ * @param packageIdentifier the package from which the document schema is from.
+ * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema
+ */
+ // Merged list available from getPubliclyVisibleSchemas
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ @NonNull
+ public Builder setPubliclyVisibleSchema(
+ @NonNull String schemaType, @NonNull PackageIdentifier packageIdentifier) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(packageIdentifier);
+ resetIfBuilt();
+ InternalVisibilityConfig.Builder visibilityConfigBuilder =
+ getOrCreateVisibilityConfigBuilder(schemaType);
+ visibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);
+ return this;
+ }
+
+ /**
+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+ *
+ * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold
+ * required permissions AND it is a specified package.
+ *
+ * <p> The querier could have access if they match ALL requirements in ANY of the given
+ * {@link SchemaVisibilityConfig}s
+ *
+ * <p>For example, if the Set contains {@code {% verbatim %}{{PackageA and Permission1},
+ * {PackageB and Permission2}}{% endverbatim %}}.
+ * <ul>
+ * <li>A querier from packageA could read if they holds Permission1.</li>
+ * <li>A querier from packageA could NOT read if they only holds Permission2 instead of
+ * Permission1.</li>
+ * <li>A querier from packageB could read if they holds Permission2.</li>
+ * <li>A querier from packageC could never read.</li>
+ * <li>A querier holds both PermissionA and PermissionE has access.</li>
+ * </ul>
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param visibleToConfigs The {@link SchemaVisibilityConfig}s hold all requirements that
+ * a call must to match to access the schema.
+ */
+ // Merged map available from getSchemasVisibleToConfigs
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @NonNull
+ public Builder setSchemaTypeVisibleToConfigs(@NonNull String schemaType,
+ @NonNull Set<SchemaVisibilityConfig> visibleToConfigs) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(visibleToConfigs);
+ resetIfBuilt();
+ InternalVisibilityConfig.Builder visibilityConfigBuilder =
+ getOrCreateVisibilityConfigBuilder(schemaType);
+ for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
+ visibilityConfigBuilder.addVisibleToConfig(visibleToConfig);
+ }
return this;
}
@@ -416,17 +565,14 @@
* @exportToFramework:hide
*/
// Visibility setting is determined by SDK version, so it won't be needed in framework
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setVisibilitySettingSupported(boolean visibilitySettingSupported) {
if (visibilitySettingSupported) {
- mSchemasNotDisplayedBySystem = new ArrayList<>();
- mSchemasVisibleToPackages = new Bundle();
- mSchemasVisibleToPermissions = new Bundle();
+ mVisibilityConfigBuilders = new ArrayMap<>();
} else {
- mSchemasNotDisplayedBySystem = null;
- mSchemasVisibleToPackages = null;
- mSchemasVisibleToPermissions = null;
+ mVisibilityConfigBuilders = null;
}
return this;
}
@@ -434,33 +580,37 @@
/** 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);
+ List<InternalVisibilityConfig> visibilityConfigs = null;
+ if (mVisibilityConfigBuilders != null) {
+ visibilityConfigs = new ArrayList<>();
+ for (InternalVisibilityConfig.Builder builder :
+ mVisibilityConfigBuilders.values()) {
+ visibilityConfigs.add(builder.build());
+ }
}
mBuilt = true;
- return new GetSchemaResponse(bundle);
+ return new GetSchemaResponse(mVersion, mSchemas, visibilityConfigs);
+ }
+
+ @NonNull
+ private InternalVisibilityConfig.Builder getOrCreateVisibilityConfigBuilder(
+ @NonNull String schemaType) {
+ if (mVisibilityConfigBuilders == null) {
+ throw new IllegalStateException("GetSchemaResponse is not configured with"
+ + "visibility setting support");
+ }
+ InternalVisibilityConfig.Builder builder = mVisibilityConfigBuilders.get(schemaType);
+ if (builder == null) {
+ builder = new InternalVisibilityConfig.Builder(schemaType);
+ mVisibilityConfigBuilders.put(schemaType, builder);
+ }
+ return builder;
}
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;
- }
+ // No need to copy mVisibilityConfigBuilders -- it gets copied during build().
+ mSchemas = new ArrayList<>(mSchemas);
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 7f9dfb0..17fbfbd 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -53,11 +53,9 @@
* @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,
@@ -127,11 +125,9 @@
// 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);
@@ -170,11 +166,9 @@
* @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_REGISTER_OBSERVER_CALLBACK)
- // @exportToFramework:endStrip()
void registerObserverCallback(
@NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@@ -204,11 +198,9 @@
* @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_REGISTER_OBSERVER_CALLBACK)
- // @exportToFramework:endStrip()
void unregisterObserverCallback(
@NonNull String targetPackageName, @NonNull ObserverCallback observer)
throws AppSearchException;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
index 3a1da37..2ece210 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
@@ -16,11 +16,17 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.InternalSetSchemaResponseCreator;
import androidx.core.util.Preconditions;
/**
@@ -34,36 +40,30 @@
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class InternalSetSchemaResponse {
-
- private static final String IS_SUCCESS_FIELD = "isSuccess";
- private static final String SET_SCHEMA_RESPONSE_BUNDLE_FIELD = "setSchemaResponseBundle";
- private static final String ERROR_MESSAGE_FIELD = "errorMessage";
-
- private final Bundle mBundle;
-
- public InternalSetSchemaResponse(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
-
- private InternalSetSchemaResponse(boolean isSuccess,
- @NonNull SetSchemaResponse setSchemaResponse,
- @Nullable String errorMessage) {
- Preconditions.checkNotNull(setSchemaResponse);
- mBundle = new Bundle();
- mBundle.putBoolean(IS_SUCCESS_FIELD, isSuccess);
- mBundle.putBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD, setSchemaResponse.getBundle());
- mBundle.putString(ERROR_MESSAGE_FIELD, errorMessage);
- }
-
- /**
- * Returns the {@link Bundle} populated by this builder.
- * @exportToFramework:hide
- */
- @NonNull
[email protected](creator = "InternalSetSchemaResponseCreator")
+public class InternalSetSchemaResponse extends AbstractSafeParcelable {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Bundle getBundle() {
- return mBundle;
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<InternalSetSchemaResponse> CREATOR =
+ new InternalSetSchemaResponseCreator();
+
+ @Field(id = 1, getter = "isSuccess")
+ private final boolean mIsSuccess;
+
+ @Field(id = 2, getter = "getSetSchemaResponse")
+ private final SetSchemaResponse mSetSchemaResponse;
+ @Field(id = 3, getter = "getErrorMessage")
+ @Nullable private final String mErrorMessage;
+
+ @Constructor
+ public InternalSetSchemaResponse(
+ @Param(id = 1) boolean isSuccess,
+ @Param(id = 2) @NonNull SetSchemaResponse setSchemaResponse,
+ @Param(id = 3) @Nullable String errorMessage) {
+ Preconditions.checkNotNull(setSchemaResponse);
+ mIsSuccess = isSuccess;
+ mSetSchemaResponse = setSchemaResponse;
+ mErrorMessage = errorMessage;
}
/**
@@ -94,7 +94,7 @@
/** Returns {@code true} if the schema request is proceeded successfully. */
public boolean isSuccess() {
- return mBundle.getBoolean(IS_SUCCESS_FIELD);
+ return mIsSuccess;
}
/**
@@ -104,7 +104,7 @@
*/
@NonNull
public SetSchemaResponse getSetSchemaResponse() {
- return new SetSchemaResponse(mBundle.getBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD));
+ return mSetSchemaResponse;
}
@@ -115,6 +115,13 @@
*/
@Nullable
public String getErrorMessage() {
- return mBundle.getString(ERROR_MESSAGE_FIELD);
+ return mErrorMessage;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ InternalSetSchemaResponseCreator.writeToParcel(this, dest, flags);
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
new file mode 100644
index 0000000..843d1cf
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2023 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.InternalVisibilityConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An expanded version of {@link SchemaVisibilityConfig} which includes fields for internal use by
+ * AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "InternalVisibilityConfigCreator")
+public final class InternalVisibilityConfig extends AbstractSafeParcelable {
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final Parcelable.Creator<InternalVisibilityConfig> CREATOR =
+ new InternalVisibilityConfigCreator();
+
+ /**
+ * Build the List of {@link InternalVisibilityConfig}s from visibility settings.
+ */
+ @NonNull
+ public static List<InternalVisibilityConfig> toInternalVisibilityConfigs(
+ @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();
+ Map<String, PackageIdentifier> publiclyVisibleSchemas =
+ setSchemaRequest.getPubliclyVisibleSchemas();
+ Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+ setSchemaRequest.getSchemasVisibleToConfigs();
+
+ List<InternalVisibilityConfig> result = new ArrayList<>(searchSchemas.size());
+ for (AppSearchSchema searchSchema : searchSchemas) {
+ String schemaType = searchSchema.getSchemaType();
+ InternalVisibilityConfig.Builder builder =
+ new InternalVisibilityConfig.Builder(schemaType)
+ .setNotDisplayedBySystem(
+ schemasNotDisplayedBySystem.contains(schemaType));
+
+ Set<PackageIdentifier> visibleToPackages = schemasVisibleToPackages.get(schemaType);
+ if (visibleToPackages != null) {
+ for (PackageIdentifier packageIdentifier : visibleToPackages) {
+ builder.addVisibleToPackage(packageIdentifier);
+ }
+ }
+
+ Set<Set<Integer>> visibleToPermissionSets = schemasVisibleToPermissions.get(schemaType);
+ if (visibleToPermissionSets != null) {
+ for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+ builder.addVisibleToPermissions(visibleToPermissions);
+ }
+ }
+
+ PackageIdentifier publiclyVisibleTargetPackage = publiclyVisibleSchemas.get(schemaType);
+ if (publiclyVisibleTargetPackage != null) {
+ builder.setPubliclyVisibleTargetPackage(publiclyVisibleTargetPackage);
+ }
+
+ Set<SchemaVisibilityConfig> visibleToConfigs = schemasVisibleToConfigs.get(schemaType);
+ if (visibleToConfigs != null) {
+ for (SchemaVisibilityConfig schemaVisibilityConfig : visibleToConfigs) {
+ builder.addVisibleToConfig(schemaVisibilityConfig);
+ }
+ }
+
+ result.add(builder.build());
+ }
+ return result;
+ }
+
+ @NonNull
+ @Field(id = 1, getter = "getSchemaType")
+ private final String mSchemaType;
+
+ @Field(id = 2, getter = "isNotDisplayedBySystem")
+ private final boolean mIsNotDisplayedBySystem;
+
+ /** The public visibility settings available in VisibilityConfig. */
+ @NonNull
+ @Field(id = 3, getter = "getVisibilityConfig")
+ private final SchemaVisibilityConfig mVisibilityConfig;
+
+ /** Extended visibility settings from {@link SetSchemaRequest#getSchemasVisibleToConfigs()} */
+ @NonNull
+ @Field(id = 4)
+ final List<SchemaVisibilityConfig> mVisibleToConfigs;
+
+ @Constructor
+ InternalVisibilityConfig(
+ @Param(id = 1) @NonNull String schemaType,
+ @Param(id = 2) boolean isNotDisplayedBySystem,
+ @Param(id = 3) @NonNull SchemaVisibilityConfig schemaVisibilityConfig,
+ @Param(id = 4) @NonNull List<SchemaVisibilityConfig> visibleToConfigs) {
+ mIsNotDisplayedBySystem = isNotDisplayedBySystem;
+ mSchemaType = Objects.requireNonNull(schemaType);
+ mVisibilityConfig = Objects.requireNonNull(schemaVisibilityConfig);
+ mVisibleToConfigs = Objects.requireNonNull(visibleToConfigs);
+ }
+
+ /**
+ * Gets the schemaType for this VisibilityConfig.
+ *
+ * <p>This is being used as the document id when we convert a {@link InternalVisibilityConfig}
+ * to a {@link GenericDocument}.
+ */
+ @NonNull
+ public String getSchemaType() {
+ return mSchemaType;
+ }
+
+ /** Returns whether this schema is visible to the system. */
+ public boolean isNotDisplayedBySystem() {
+ return mIsNotDisplayedBySystem;
+ }
+
+ /**
+ * Returns the visibility settings stored in the public {@link SchemaVisibilityConfig} object.
+ */
+ @NonNull
+ public SchemaVisibilityConfig getVisibilityConfig() {
+ return mVisibilityConfig;
+ }
+
+ /**
+ * Returns required {@link SchemaVisibilityConfig} sets for a caller need to match to access the
+ * schema this {@link InternalVisibilityConfig} represents.
+ */
+ @NonNull
+ public Set<SchemaVisibilityConfig> getVisibleToConfigs() {
+ return new ArraySet<>(mVisibleToConfigs);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ InternalVisibilityConfigCreator.writeToParcel(this, dest, flags);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null) {
+ return false;
+ }
+ if (!(o instanceof InternalVisibilityConfig)) {
+ return false;
+ }
+ InternalVisibilityConfig that = (InternalVisibilityConfig) o;
+ return mIsNotDisplayedBySystem == that.mIsNotDisplayedBySystem
+ && Objects.equals(mSchemaType, that.mSchemaType)
+ && Objects.equals(mVisibilityConfig, that.mVisibilityConfig)
+ && Objects.equals(mVisibleToConfigs, that.mVisibleToConfigs);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsNotDisplayedBySystem, mSchemaType, mVisibilityConfig,
+ mVisibleToConfigs);
+ }
+
+ /** The builder class of {@link InternalVisibilityConfig}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ public static final class Builder {
+ private String mSchemaType;
+ private boolean mIsNotDisplayedBySystem;
+ private SchemaVisibilityConfig.Builder mVisibilityConfigBuilder;
+ private List<SchemaVisibilityConfig> mVisibleToConfigs = new ArrayList<>();
+ private boolean mBuilt;
+
+ /**
+ * Creates a {@link Builder} for a {@link InternalVisibilityConfig}.
+ *
+ * @param schemaType The SchemaType of the {@link AppSearchSchema} that this {@link
+ * InternalVisibilityConfig} 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 InternalVisibilityConfig}. This will be
+ * used as as an AppSearch id.
+ * @see GenericDocument#getId
+ */
+ public Builder(@NonNull String schemaType) {
+ mSchemaType = Objects.requireNonNull(schemaType);
+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder();
+ }
+
+ /** Creates a {@link Builder} from an existing {@link InternalVisibilityConfig} */
+ public Builder(@NonNull InternalVisibilityConfig internalVisibilityConfig) {
+ Objects.requireNonNull(internalVisibilityConfig);
+ mSchemaType = internalVisibilityConfig.mSchemaType;
+ mIsNotDisplayedBySystem = internalVisibilityConfig.mIsNotDisplayedBySystem;
+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(
+ internalVisibilityConfig.getVisibilityConfig());
+ mVisibleToConfigs = internalVisibilityConfig.mVisibleToConfigs;
+ }
+
+ /** Sets schemaType, which will be as the id when converting to {@link GenericDocument}. */
+ @NonNull
+ @CanIgnoreReturnValue
+ public Builder setSchemaType(@NonNull String schemaType) {
+ resetIfBuilt();
+ mSchemaType = Objects.requireNonNull(schemaType);
+ return this;
+ }
+
+ /**
+ * Resets all values contained in the VisibilityConfig with the values from the given
+ * VisibiltiyConfig.
+ */
+ @NonNull
+ @CanIgnoreReturnValue
+ public Builder setVisibilityConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+ resetIfBuilt();
+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(schemaVisibilityConfig);
+ return this;
+ }
+
+ /**
+ * Sets whether this schema has opted out of platform surfacing.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+ resetIfBuilt();
+ mIsNotDisplayedBySystem = notDisplayedBySystem;
+ return this;
+ }
+
+ /**
+ * Add {@link PackageIdentifier} of packages which has access to this schema.
+ *
+ * @see SchemaVisibilityConfig.Builder#addAllowedPackage
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+ resetIfBuilt();
+ mVisibilityConfigBuilder.addAllowedPackage(packageIdentifier);
+ return this;
+ }
+
+ /**
+ * Clears the list of packages which have access to this schema.
+ *
+ * @see SchemaVisibilityConfig.Builder#clearAllowedPackages
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearVisibleToPackages() {
+ resetIfBuilt();
+ mVisibilityConfigBuilder.clearAllowedPackages();
+ return this;
+ }
+
+ /**
+ * Adds a set of required Android {@link android.Manifest.permission} combination a package
+ * needs to hold to access the schema.
+ *
+ * @see SchemaVisibilityConfig.Builder#addRequiredPermissions
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
+ resetIfBuilt();
+ mVisibilityConfigBuilder.addRequiredPermissions(visibleToPermissions);
+ return this;
+ }
+
+ /**
+ * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.
+ *
+ * @see SchemaVisibilityConfig.Builder#clearRequiredPermissions
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearVisibleToPermissions() {
+ resetIfBuilt();
+ mVisibilityConfigBuilder.clearRequiredPermissions();
+ return this;
+ }
+
+ /**
+ * Specify that this schema should be publicly available, to the same packages that have
+ * visibility to the package passed as a parameter. This visibility is determined by the
+ * result of {@link android.content.pm.PackageManager#canPackageQuery}.
+ *
+ * @see SchemaVisibilityConfig.Builder#setPubliclyVisibleTargetPackage
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setPubliclyVisibleTargetPackage(
+ @Nullable PackageIdentifier packageIdentifier) {
+ resetIfBuilt();
+ mVisibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);
+ return this;
+ }
+
+ /**
+ * Add the {@link SchemaVisibilityConfig} for a caller need to match to access the schema
+ * this {@link InternalVisibilityConfig} represents.
+ *
+ * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig},
+ * and the querier will have access if they match ANY of the
+ * {@link SchemaVisibilityConfig}.
+ *
+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} hold all requirements
+ * that a call must match to access the schema.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addVisibleToConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+ Objects.requireNonNull(schemaVisibilityConfig);
+ resetIfBuilt();
+ mVisibleToConfigs.add(schemaVisibilityConfig);
+ return this;
+ }
+
+ /** Clears the set of {@link SchemaVisibilityConfig} which have access to this schema. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearVisibleToConfig() {
+ resetIfBuilt();
+ mVisibleToConfigs.clear();
+ return this;
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mVisibleToConfigs = new ArrayList<>(mVisibleToConfigs);
+ mBuilt = false;
+ }
+ }
+
+ /** Build a {@link InternalVisibilityConfig} */
+ @NonNull
+ public InternalVisibilityConfig build() {
+ mBuilt = true;
+ return new InternalVisibilityConfig(
+ mSchemaType,
+ mIsNotDisplayedBySystem,
+ mVisibilityConfigBuilder.build(),
+ mVisibleToConfigs);
+ }
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
new file mode 100644
index 0000000..bd13255
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.File;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Contains utility methods for Framework implementation of AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JetpackAppSearchEnvironment implements AppSearchEnvironment {
+
+ /**
+ * Returns AppSearch directory in the credential encrypted system directory for the given user.
+ *
+ * <p>This folder should only be accessed after unlock.
+ */
+ @Override
+ @NonNull
+ public File getAppSearchDir(@NonNull Context context, @Nullable UserHandle unused) {
+ return new File(context.getFilesDir(), "appsearch");
+ }
+
+ /** Creates context for the user based on the userHandle. */
+ @Override
+ @NonNull
+ public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) {
+ return context;
+ }
+
+ /** Creates and returns a ThreadPoolExecutor for given parameters. */
+ @Override
+ @NonNull
+ public ExecutorService createExecutorService(
+ int corePoolSize,
+ int maxConcurrency,
+ long keepAliveTime,
+ @NonNull TimeUnit unit,
+ @NonNull BlockingQueue<Runnable> workQueue,
+ int priority) {
+ return new ThreadPoolExecutor(
+ corePoolSize,
+ maxConcurrency,
+ keepAliveTime,
+ unit,
+ workQueue);
+ }
+
+ /** Creates and returns an ExecutorService with a single thread. */
+ @Override
+ @NonNull
+ public ExecutorService createSingleThreadExecutor() {
+ return Executors.newSingleThreadExecutor();
+ }
+
+ /** Creates and returns an Executor with cached thread pools. */
+ @Override
+ @NonNull
+ public ExecutorService createCachedThreadPoolExecutor() {
+ return Executors.newCachedThreadPool();
+ }
+
+ /**
+ * Returns a cache directory for creating temporary files like in case of migrating documents.
+ */
+ @Override
+ @Nullable
+ public File getCacheDir(@NonNull Context context) {
+ return context.getCacheDir();
+ }
+
+ @Override
+ public boolean isInfoLoggingEnabled() {
+ // INFO logging is enabled by default in Jetpack AppSearch.
+ return true;
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
index 3639a79..16aff14 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
@@ -16,16 +16,23 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.JoinSpecCreator;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
/**
* This class represents the specifications for the joining operation in search.
@@ -117,12 +124,29 @@
* return the signals calculated by scoring the joined documents using the scoring strategy in the
* nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}.
*/
-public final class JoinSpec {
- static final String NESTED_QUERY = "nestedQuery";
- static final String NESTED_SEARCH_SPEC = "nestedSearchSpec";
- static final String CHILD_PROPERTY_EXPRESSION = "childPropertyExpression";
- static final String MAX_JOINED_RESULT_COUNT = "maxJoinedResultCount";
- static final String AGGREGATION_SCORING_STRATEGY = "aggregationScoringStrategy";
[email protected](creator = "JoinSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class JoinSpec extends AbstractSafeParcelable {
+ /** Creator class for {@link JoinSpec}. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<JoinSpec> CREATOR = new JoinSpecCreator();
+
+ @Field(id = 1, getter = "getNestedQuery")
+ private final String mNestedQuery;
+
+ @Field(id = 2, getter = "getNestedSearchSpec")
+ private final SearchSpec mNestedSearchSpec;
+
+ @Field(id = 3, getter = "getChildPropertyExpression")
+ private final String mChildPropertyExpression;
+
+ @Field(id = 4, getter = "getMaxJoinedResultCount")
+ private final int mMaxJoinedResultCount;
+
+ @Field(id = 5, getter = "getAggregationScoringStrategy")
+ private final int mAggregationScoringStrategy;
private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10;
@@ -158,8 +182,10 @@
public @interface AggregationScoringStrategy {
}
- /** Do not score the aggregation of joined documents. This is for the case where we want to
- * perform a join, but keep the parent ranking signal. */
+ /**
+ * Do not score the aggregation of joined documents. This is for the case where we want to
+ * perform a join, but keep the parent ranking signal.
+ */
public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0;
/** Score the aggregation of joined documents by counting the number of results. */
public static final int AGGREGATION_SCORING_RESULT_COUNT = 1;
@@ -172,33 +198,27 @@
/** Score the aggregation of joined documents using the sum of ranking signal. */
public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;
- private final Bundle mBundle;
-
- /** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public JoinSpec(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
+ @Constructor
+ JoinSpec(
+ @Param(id = 1) @NonNull String nestedQuery,
+ @Param(id = 2) @NonNull SearchSpec nestedSearchSpec,
+ @Param(id = 3) @NonNull String childPropertyExpression,
+ @Param(id = 4) int maxJoinedResultCount,
+ @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy) {
+ mNestedQuery = Objects.requireNonNull(nestedQuery);
+ mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec);
+ mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression);
+ mMaxJoinedResultCount = maxJoinedResultCount;
+ mAggregationScoringStrategy = aggregationScoringStrategy;
}
- /**
- * Returns the {@link Bundle} populated by this builder.
- *
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
- }
/**
* Returns the query to run on the joined documents.
- *
*/
@NonNull
public String getNestedQuery() {
- return mBundle.getString(NESTED_QUERY);
+ return mNestedQuery;
}
/**
@@ -211,7 +231,7 @@
*/
@NonNull
public String getChildPropertyExpression() {
- return mBundle.getString(CHILD_PROPERTY_EXPRESSION);
+ return mChildPropertyExpression;
}
/**
@@ -219,7 +239,7 @@
* with a default of 10 SearchResults.
*/
public int getMaxJoinedResultCount() {
- return mBundle.getInt(MAX_JOINED_RESULT_COUNT);
+ return mMaxJoinedResultCount;
}
/**
@@ -231,7 +251,7 @@
*/
@NonNull
public SearchSpec getNestedSearchSpec() {
- return new SearchSpec(mBundle.getBundle(NESTED_SEARCH_SPEC));
+ return mNestedSearchSpec;
}
/**
@@ -244,7 +264,14 @@
*/
@AggregationScoringStrategy
public int getAggregationScoringStrategy() {
- return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);
+ return mAggregationScoringStrategy;
+ }
+
+ @Override
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ JoinSpecCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link JoinSpec objects}. */
@@ -257,7 +284,8 @@
private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
private final String mChildPropertyExpression;
private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
- @AggregationScoringStrategy private int mAggregationScoringStrategy =
+ @AggregationScoringStrategy
+ private int mAggregationScoringStrategy =
AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
/**
@@ -289,6 +317,17 @@
mChildPropertyExpression = childPropertyExpression;
}
+ /** @exportToFramework:hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull JoinSpec joinSpec) {
+ Preconditions.checkNotNull(joinSpec);
+ mNestedQuery = joinSpec.getNestedQuery();
+ mNestedSearchSpec = joinSpec.getNestedSearchSpec();
+ mChildPropertyExpression = joinSpec.getChildPropertyExpression();
+ mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount();
+ mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy();
+ }
+
/**
* Sets the query and the SearchSpec for the documents being joined. This will score and
* rank the joined documents as well as filter the joined documents.
@@ -362,13 +401,13 @@
*/
@NonNull
public JoinSpec build() {
- Bundle bundle = new Bundle();
- bundle.putString(NESTED_QUERY, mNestedQuery);
- bundle.putBundle(NESTED_SEARCH_SPEC, mNestedSearchSpec.getBundle());
- bundle.putString(CHILD_PROPERTY_EXPRESSION, mChildPropertyExpression);
- bundle.putInt(MAX_JOINED_RESULT_COUNT, mMaxJoinedResultCount);
- bundle.putInt(AGGREGATION_SCORING_STRATEGY, mAggregationScoringStrategy);
- return new JoinSpec(bundle);
+ return new JoinSpec(
+ mNestedQuery,
+ mNestedSearchSpec,
+ mChildPropertyExpression,
+ mMaxJoinedResultCount,
+ mAggregationScoringStrategy
+ );
}
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
index 6c4bc59..c05d567 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -16,20 +16,18 @@
package androidx.appsearch.app;
-import android.os.Bundle;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.safeparcel.PackageIdentifierParcel;
import androidx.core.util.Preconditions;
-/** This class represents a uniquely identifiable package. */
+/**
+ * This class represents a uniquely identifiable package.
+ */
public class PackageIdentifier {
- private static final String PACKAGE_NAME_FIELD = "packageName";
- private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
-
- private final Bundle mBundle;
+ @NonNull
+ private final PackageIdentifierParcel mPackageIdentifierParcel;
/**
* Creates a unique identifier for a package.
@@ -43,36 +41,41 @@
* new android.content.pm.Signature(outputDigest).toByteArray();
* </pre>
*
- * @param packageName Name of the package.
+ * @param packageName Name of the package.
* @param sha256Certificate SHA-256 certificate digest of the package.
*/
public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
- mBundle = new Bundle();
- mBundle.putString(PACKAGE_NAME_FIELD, packageName);
- mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(sha256Certificate);
+ mPackageIdentifierParcel = new PackageIdentifierParcel(packageName, sha256Certificate);
}
/** @exportToFramework:hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public PackageIdentifier(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
+ public PackageIdentifier(@NonNull PackageIdentifierParcel packageIdentifierParcel) {
+ mPackageIdentifierParcel = Preconditions.checkNotNull(packageIdentifierParcel);
}
- /** @exportToFramework:hide */
+ /**
+ * Returns the {@link PackageIdentifierParcel} holding the values for this
+ * {@link PackageIdentifier}.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public Bundle getBundle() {
- return mBundle;
+ public PackageIdentifierParcel getPackageIdentifierParcel() {
+ return mPackageIdentifierParcel;
}
@NonNull
public String getPackageName() {
- return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
+ return mPackageIdentifierParcel.getPackageName();
}
@NonNull
public byte[] getSha256Certificate() {
- return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
+ return mPackageIdentifierParcel.getSha256Certificate();
}
@Override
@@ -80,15 +83,15 @@
if (this == obj) {
return true;
}
- if (obj == null || !(obj instanceof PackageIdentifier)) {
+ if (!(obj instanceof PackageIdentifier)) {
return false;
}
final PackageIdentifier other = (PackageIdentifier) obj;
- return BundleUtil.deepEquals(mBundle, other.mBundle);
+ return mPackageIdentifierParcel.equals(other.getPackageIdentifierParcel());
}
@Override
public int hashCode() {
- return BundleUtil.deepHashCode(mBundle);
+ return mPackageIdentifierParcel.hashCode();
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
index e0557a6..0e69136 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
@@ -20,6 +20,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appsearch.checker.initialization.qual.UnderInitialization;
+import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
@@ -73,7 +75,9 @@
}
}
- private void recursivePathScan(String path) throws IllegalArgumentException {
+ @RequiresNonNull("mPathList")
+ private void recursivePathScan(@UnderInitialization PropertyPath this, String path)
+ throws IllegalArgumentException {
// Determine whether the path is just a raw property name with no control characters
int controlPos = -1;
boolean controlIsIndex = false;
@@ -128,7 +132,9 @@
* @return the rest of the path after the end brackets, or null if there is nothing after them
*/
@Nullable
- private String consumePropertyWithIndex(@NonNull String path, int controlPos) {
+ @RequiresNonNull("mPathList")
+ private String consumePropertyWithIndex(
+ @UnderInitialization PropertyPath this, @NonNull String path, int controlPos) {
Preconditions.checkNotNull(path);
String propertyName = path.substring(0, controlPos);
int endBracketIdx = path.indexOf(']', controlPos);
@@ -210,17 +216,23 @@
}
@Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null) return false;
- if (!(o instanceof PropertyPath)) return false;
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null) {
+ return false;
+ }
+ if (!(o instanceof PropertyPath)) {
+ return false;
+ }
PropertyPath that = (PropertyPath) o;
return ObjectsCompat.equals(mPathList, that.mPathList);
}
@Override
public int hashCode() {
- return ObjectsCompat.hash(mPathList);
+ return ObjectsCompat.hashCode(mPathList);
}
/**
@@ -292,9 +304,7 @@
mPropertyIndex = propertyIndex;
}
- /**
- * @return the property name
- */
+ /** Returns the name of the property. */
@NonNull
public String getPropertyName() {
return mPropertyName;
@@ -319,10 +329,16 @@
}
@Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null) return false;
- if (!(o instanceof PathSegment)) return false;
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null) {
+ return false;
+ }
+ if (!(o instanceof PathSegment)) {
+ return false;
+ }
PathSegment that = (PathSegment) o;
return mPropertyIndex == that.mPropertyIndex
&& mPropertyName.equals(that.mPropertyName);
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 6edf85e..e0332b4 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -19,8 +19,13 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.usagereporting.TakenAction;
+import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
@@ -28,6 +33,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/**
* Encapsulates a request to index documents into an {@link AppSearchSession} database.
@@ -43,8 +49,11 @@
public final class PutDocumentsRequest {
private final List<GenericDocument> mDocuments;
- PutDocumentsRequest(List<GenericDocument> documents) {
+ private final List<GenericDocument> mTakenActions;
+
+ PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions) {
mDocuments = documents;
+ mTakenActions = takenActions;
}
/** Returns a list of {@link GenericDocument} objects that are part of this request. */
@@ -53,9 +62,27 @@
return Collections.unmodifiableList(mDocuments);
}
+ /**
+ * Returns a list of {@link GenericDocument} objects containing taken action metrics that are
+ * part of this request.
+ *
+ * <!--@exportToFramework:ifJetpack()-->
+ * <p>See {@link Builder#addTakenActions(TakenAction...)}.
+ * <!--@exportToFramework:else()
+ * <p>See {@link Builder#addTakenActionGenericDocuments(GenericDocument...)}.
+ * -->
+ */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+ public List<GenericDocument> getTakenActionGenericDocuments() {
+ return Collections.unmodifiableList(mTakenActions);
+ }
+
+
/** Builder for {@link PutDocumentsRequest} objects. */
public static final class Builder {
private ArrayList<GenericDocument> mDocuments = new ArrayList<>();
+ private ArrayList<GenericDocument> mTakenActions = new ArrayList<>();
private boolean mBuilt = false;
/** Adds one or more {@link GenericDocument} objects to the request. */
@@ -121,18 +148,219 @@
}
return addGenericDocuments(genericDocuments);
}
+
+ /**
+ * Adds one or more {@link TakenAction} objects to the request.
+ *
+ * <p>Clients can construct {@link TakenAction} documents to report the user's actions on
+ * search results, and these actions can be used as signals to boost result ranking in
+ * future search requests. See {@link TakenAction} for more details.
+ *
+ * <p>Clients should report search and click actions together sorted by
+ * {@link TakenAction#getActionTimestampMillis} in chronological order.
+ * <p>For example, if there are 2 search actions, with 1 click action associated with the
+ * first and 2 click actions associated with the second, then clients should report
+ * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
+ *
+ * <p>Certain anonymized information about actions reported using this API may be uploaded
+ * using statsd and may be used to improve the quality of the search algorithms. Most of
+ * the information in this class is already non-identifiable, such as durations and its
+ * position in the result set. Identifiable information which you choose to provide, such
+ * as the query string, will be anonymized using techniques like Federated Analytics to
+ * ensure only the most frequently searched terms across the whole user population are
+ * retained and available for study.
+ *
+ * <p>You can alternatively use the {@link #addDocuments(Object...)} API with
+ * {@link TakenAction} document to retain the benefits of joining and using it on-device,
+ * without triggering any of the anonymized stats uploading described above.
+ *
+ * @param takenActions one or more {@link TakenAction} objects.
+ */
+ // Merged list available from getTakenActionGenericDocuments()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addTakenActions(
+ @NonNull TakenAction... takenActions) throws AppSearchException {
+ Preconditions.checkNotNull(takenActions);
+ resetIfBuilt();
+ return addTakenActions(Arrays.asList(takenActions));
+ }
+
+ /**
+ * Adds a collection of {@link TakenAction} objects to the request.
+ *
+ * @see #addTakenActions(TakenAction...)
+ *
+ * @param takenActions a collection of {@link TakenAction} objects.
+ */
+ // Merged list available from getTakenActionGenericDocuments()
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addTakenActions(@NonNull Collection<? extends TakenAction> takenActions)
+ throws AppSearchException {
+ Preconditions.checkNotNull(takenActions);
+ resetIfBuilt();
+ List<GenericDocument> genericDocuments = new ArrayList<>(takenActions.size());
+ for (Object takenAction : takenActions) {
+ GenericDocument genericDocument = GenericDocument.fromDocumentClass(takenAction);
+ genericDocuments.add(genericDocument);
+ }
+ mTakenActions.addAll(genericDocuments);
+ return this;
+ }
// @exportToFramework:endStrip()
- /** Creates a new {@link PutDocumentsRequest} object. */
+ /**
+ * Adds one or more {@link GenericDocument} objects containing taken action metrics to the
+ * request.
+ *
+ * <p>It is recommended to use taken action document classes in Jetpack library to construct
+ * taken action documents.
+ *
+ * <p>The document creation timestamp of the {@link GenericDocument} should be set to the
+ * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}.
+ *
+ * <p>Clients should report search and click actions together sorted by
+ * {@link GenericDocument#getCreationTimestampMillis} in chronological order.
+ * <p>For example, if there are 2 search actions, with 1 click action associated with the
+ * first and 2 click actions associated with the second, then clients should report
+ * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
+ *
+ * <p>Different types of taken actions and metrics to be collected by AppSearch:
+ * <ul>
+ * <li>
+ * Search action
+ * <ul>
+ * <li>actionType: LONG, the enum value of the action type.
+ * <p>Requires to be {@code 1} for search actions.
+ *
+ * <li>query: STRING, the user-entered search input (without any operators or
+ * rewriting).
+ *
+ * <li>fetchedResultCount: LONG, the number of {@link SearchResult} documents
+ * fetched from AppSearch in this search action.
+ * </ul>
+ * </li>
+ *
+ * <li>
+ * Click action
+ * <ul>
+ * <li>actionType: LONG, the enum value of the action type.
+ * <p>Requires to be {@code 2} for click actions.
+ *
+ * <li>query: STRING, the user-entered search input (without any operators or
+ * rewriting) that yielded the {@link SearchResult} on which the user took action.
+ *
+ * <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
+ * document that the user takes action on.
+ * <p>A qualified id is a string generated by package, database, namespace, and
+ * document id. See
+ * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId} for more
+ * details.
+ *
+ * <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among
+ * the user-defined block.
+ * <p>The client can define its own custom definition for block, for example,
+ * corpus name, group, etc.
+ * <p>For example, a client defines the block as corpus, and AppSearch returns 5
+ * documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"].
+ * Then the block ranks of them = [1, 2, 1, 1, 2].
+ * <p>If the client is not presenting the results in multiple blocks, they should
+ * set this value to match resultRankGlobal.
+ *
+ * <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult}
+ * document.
+ * <p>Global rank reflects the order of {@link SearchResult} documents returned by
+ * AppSearch.
+ * <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult} documents
+ * for each page. Then the global ranks of them will be 1 to 10 for the first page,
+ * and 11 to 20 for the second page.
+ *
+ * <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on
+ * the {@link SearchResult} document after clicking it.
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * <p>Certain anonymized information about actions reported using this API may be uploaded
+ * using statsd and may be used to improve the quality of the search algorithms. Most of
+ * the information in this class is already non-identifiable, such as durations and its
+ * position in the result set. Identifiable information which you choose to provide, such
+ * as the query string, will be anonymized using techniques like Federated Analytics to
+ * ensure only the most frequently searched terms across the whole user population are
+ * retained and available for study.
+ *
+ * <p>You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to
+ * retain the benefits of joining and using it on-device, without triggering any of the
+ * anonymized stats uploading described above.
+ *
+ * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing
+ * taken action metric fields.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @CanIgnoreReturnValue
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+ public Builder addTakenActionGenericDocuments(
+ @NonNull GenericDocument... takenActionGenericDocuments) throws AppSearchException {
+ Preconditions.checkNotNull(takenActionGenericDocuments);
+ resetIfBuilt();
+ return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments));
+ }
+
+ /**
+ * Adds a collection of {@link GenericDocument} objects containing taken action metrics to
+ * the request.
+ *
+ * @see #addTakenActionGenericDocuments(GenericDocument...)
+ *
+ * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects
+ * containing taken action metric fields.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @CanIgnoreReturnValue
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+ public Builder addTakenActionGenericDocuments(@NonNull Collection<?
+ extends GenericDocument> takenActionGenericDocuments) throws AppSearchException {
+ Preconditions.checkNotNull(takenActionGenericDocuments);
+ resetIfBuilt();
+ mTakenActions.addAll(takenActionGenericDocuments);
+ return this;
+ }
+
+ /**
+ * Creates a new {@link PutDocumentsRequest} object.
+ *
+ * @throws IllegalArgumentException if there is any id collision between normal and action
+ * documents.
+ */
@NonNull
public PutDocumentsRequest build() {
mBuilt = true;
- return new PutDocumentsRequest(mDocuments);
+
+ // Verify there is no id collision between normal documents and action documents.
+ Set<String> idSet = new ArraySet<>();
+ for (int i = 0; i < mDocuments.size(); i++) {
+ idSet.add(mDocuments.get(i).getId());
+ }
+ for (int i = 0; i < mTakenActions.size(); i++) {
+ GenericDocument takenAction = mTakenActions.get(i);
+ if (idSet.contains(takenAction.getId())) {
+ throw new IllegalArgumentException("Document id " + takenAction.getId()
+ + " cannot exist in both taken action and normal document");
+ }
+ }
+
+ return new PutDocumentsRequest(mDocuments, mTakenActions);
}
private void resetIfBuilt() {
if (mBuilt) {
mDocuments = new ArrayList<>(mDocuments);
+ mTakenActions = new ArrayList<>(mTakenActions);
mBuilt = false;
}
}
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 40cd591..ce76455 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -16,14 +16,27 @@
package androidx.appsearch.app;
+import android.os.Parcel;
+import android.os.Parcelable;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.RemoveByDocumentIdRequestCreator;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -32,13 +45,37 @@
*
* @see AppSearchSession#removeAsync
*/
-public final class RemoveByDocumentIdRequest {
- private final String mNamespace;
- private final Set<String> mIds;
[email protected](creator = "RemoveByDocumentIdRequestCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class RemoveByDocumentIdRequest extends AbstractSafeParcelable {
+ /** Creator class for {@link android.app.appsearch.RemoveByDocumentIdRequest}. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<RemoveByDocumentIdRequest> CREATOR =
+ new RemoveByDocumentIdRequestCreator();
- RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
- mNamespace = namespace;
- mIds = ids;
+ @NonNull
+ @Field(id = 1, getter = "getNamespace")
+ private final String mNamespace;
+ @NonNull
+ @Field(id = 2)
+ final List<String> mIds;
+ @Nullable
+ private Set<String> mIdsCached;
+
+ /**
+ * Removes documents by ID.
+ *
+ * @param namespace Namespace of the document to remove.
+ * @param ids The IDs of the documents to delete
+ */
+ @Constructor
+ RemoveByDocumentIdRequest(
+ @Param(id = 1) @NonNull String namespace,
+ @Param(id = 2) @NonNull List<String> ids) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mIds = Objects.requireNonNull(ids);
}
/** Returns the namespace to remove documents from. */
@@ -50,7 +87,17 @@
/** Returns the set of document IDs attached to the request. */
@NonNull
public Set<String> getIds() {
- return Collections.unmodifiableSet(mIds);
+ if (mIdsCached == null) {
+ mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+ }
+ return mIdsCached;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ RemoveByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link RemoveByDocumentIdRequest} objects. */
@@ -87,7 +134,7 @@
@NonNull
public RemoveByDocumentIdRequest build() {
mBuilt = true;
- return new RemoveByDocumentIdRequest(mNamespace, mIds);
+ return new RemoveByDocumentIdRequest(mNamespace, new ArrayList<>(mIds));
}
private void resetIfBuilt() {
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 58e7d9b..aafcc61 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -16,10 +16,21 @@
package androidx.appsearch.app;
+import android.os.Parcel;
+import android.os.Parcelable;
+
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.ReportUsageRequestCreator;
import androidx.core.util.Preconditions;
+import java.util.Objects;
+
/**
* A request to report usage of a document.
*
@@ -27,18 +38,34 @@
*
* @see AppSearchSession#reportUsageAsync
*/
-public final class ReportUsageRequest {
- private final String mNamespace;
- private final String mDocumentId;
- private final long mUsageTimestampMillis;
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "ReportUsageRequestCreator")
+public final class ReportUsageRequest extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<ReportUsageRequest> CREATOR =
+ new ReportUsageRequestCreator();
+ @NonNull
+ @Field(id = 1, getter = "getNamespace")
+ private final String mNamespace;
+ @NonNull
+ @Field(id = 2, getter = "getDocumentId")
+ private final String mDocumentId;
+ @Field(id = 3, getter = "getUsageTimestampMillis")
+ private final long mUsageTimestampMillis;
+
+ @Constructor
ReportUsageRequest(
- @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
- mNamespace = Preconditions.checkNotNull(namespace);
- mDocumentId = Preconditions.checkNotNull(documentId);
+ @Param(id = 1) @NonNull String namespace,
+ @Param(id = 2) @NonNull String documentId,
+ @Param(id = 3) long usageTimestampMillis) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mDocumentId = Objects.requireNonNull(documentId);
mUsageTimestampMillis = usageTimestampMillis;
}
+
/** Returns the namespace of the document that was used. */
@NonNull
public String getNamespace() {
@@ -62,6 +89,13 @@
return mUsageTimestampMillis;
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ ReportUsageRequestCreator.writeToParcel(this, dest, flags);
+ }
+
/** Builder for {@link ReportUsageRequest} objects. */
public static final class Builder {
private final String mNamespace;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
new file mode 100644
index 0000000..32b69b3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.PackageIdentifierParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class to hold a all necessary Visibility information corresponding to the same schema. This
+ * pattern allows for easier association of these documents.
+ *
+ * <p> This does not correspond to any schema, the properties held in this class are kept in two
+ * separate schemas, VisibilityConfig and PublicAclOverlay.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
[email protected](creator = "VisibilityConfigCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SchemaVisibilityConfig extends AbstractSafeParcelable {
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final Parcelable.Creator<SchemaVisibilityConfig> CREATOR =
+ new VisibilityConfigCreator();
+
+ @NonNull
+ @Field(id = 1)
+ final List<PackageIdentifierParcel> mAllowedPackages;
+
+ @NonNull
+ @Field(id = 2)
+ final List<VisibilityPermissionConfig> mRequiredPermissions;
+
+ @Nullable
+ @Field(id = 3)
+ final PackageIdentifierParcel mPubliclyVisibleTargetPackage;
+
+ @Nullable private Integer mHashCode;
+ @Nullable private List<PackageIdentifier> mAllowedPackagesCached;
+ @Nullable private Set<Set<Integer>> mRequiredPermissionsCached;
+
+ @Constructor
+ SchemaVisibilityConfig(
+ @Param(id = 1) @NonNull List<PackageIdentifierParcel> allowedPackages,
+ @Param(id = 2) @NonNull List<VisibilityPermissionConfig> requiredPermissions,
+ @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage) {
+ mAllowedPackages = Objects.requireNonNull(allowedPackages);
+ mRequiredPermissions = Objects.requireNonNull(requiredPermissions);
+ mPubliclyVisibleTargetPackage = publiclyVisibleTargetPackage;
+ }
+
+ /** Returns a list of {@link PackageIdentifier}s of packages that can access this schema. */
+ @NonNull
+ public List<PackageIdentifier> getAllowedPackages() {
+ if (mAllowedPackagesCached == null) {
+ mAllowedPackagesCached = new ArrayList<>(mAllowedPackages.size());
+ for (int i = 0; i < mAllowedPackages.size(); i++) {
+ mAllowedPackagesCached.add(new PackageIdentifier(mAllowedPackages.get(i)));
+ }
+ }
+ return mAllowedPackagesCached;
+ }
+
+ /**
+ * Returns an array of Integers representing Android Permissions that the caller must hold to
+ * access the schema this {@link SchemaVisibilityConfig} represents.
+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
+ */
+ @NonNull
+ public Set<Set<Integer>> getRequiredPermissions() {
+ if (mRequiredPermissionsCached == null) {
+ mRequiredPermissionsCached = new ArraySet<>(mRequiredPermissions.size());
+ for (int i = 0; i < mRequiredPermissions.size(); i++) {
+ VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i);
+ Set<Integer> requiredPermissions = permissionConfig.getAllRequiredPermissions();
+ if (mRequiredPermissionsCached != null && requiredPermissions != null) {
+ mRequiredPermissionsCached.add(requiredPermissions);
+ }
+ }
+ }
+ // Added for nullness checker as it is @Nullable, we initialize it above if it is null.
+ return Objects.requireNonNull(mRequiredPermissionsCached);
+ }
+
+ /**
+ * Returns the {@link PackageIdentifier} of the package that will be used as the target package
+ * in a call to {@link android.content.pm.PackageManager#canPackageQuery} to determine which
+ * packages can access this publicly visible schema. Returns null if the schema is not publicly
+ * visible.
+ */
+ @Nullable
+ public PackageIdentifier getPubliclyVisibleTargetPackage() {
+ if (mPubliclyVisibleTargetPackage == null) {
+ return null;
+ }
+ return new PackageIdentifier(mPubliclyVisibleTargetPackage);
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ VisibilityConfigCreator.writeToParcel(this, dest, flags);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null) {
+ return false;
+ }
+ if (!(o instanceof SchemaVisibilityConfig)) {
+ return false;
+ }
+ SchemaVisibilityConfig that = (SchemaVisibilityConfig) o;
+ return Objects.equals(mAllowedPackages, that.mAllowedPackages)
+ && Objects.equals(mRequiredPermissions, that.mRequiredPermissions)
+ && Objects.equals(
+ mPubliclyVisibleTargetPackage, that.mPubliclyVisibleTargetPackage);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = Objects.hash(
+ mAllowedPackages,
+ mRequiredPermissions,
+ mPubliclyVisibleTargetPackage);
+ }
+ return mHashCode;
+ }
+
+ /** The builder class of {@link SchemaVisibilityConfig}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ public static final class Builder {
+ private List<PackageIdentifierParcel> mAllowedPackages = new ArrayList<>();
+ private List<VisibilityPermissionConfig> mRequiredPermissions = new ArrayList<>();
+ @Nullable private PackageIdentifierParcel mPubliclyVisibleTargetPackage;
+ private boolean mBuilt;
+
+ /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */
+ public Builder() {}
+
+ /**
+ * Creates a {@link Builder} copying the values from an existing
+ * {@link SchemaVisibilityConfig}.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+ Objects.requireNonNull(schemaVisibilityConfig);
+ mAllowedPackages = new ArrayList<>(schemaVisibilityConfig.mAllowedPackages);
+ mRequiredPermissions = new ArrayList<>(schemaVisibilityConfig.mRequiredPermissions);
+ mPubliclyVisibleTargetPackage = schemaVisibilityConfig.mPubliclyVisibleTargetPackage;
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addAllowedPackage(@NonNull PackageIdentifier packageIdentifier) {
+ Objects.requireNonNull(packageIdentifier);
+ resetIfBuilt();
+ mAllowedPackages.add(packageIdentifier.getPackageIdentifierParcel());
+ return this;
+ }
+
+ /** Clears the list of packages which have access to this schema. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearAllowedPackages() {
+ resetIfBuilt();
+ mAllowedPackages.clear();
+ return this;
+ }
+
+ /**
+ * Adds a set of required Android {@link android.Manifest.permission} combination a
+ * package needs to hold to access the schema this {@link SchemaVisibilityConfig}
+ * represents.
+ *
+ * <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 repeatedly to add multiple permission combinations, and the
+ * querier will have access if they holds ANY of the combinations.
+ *
+ * <p>Merged Set available from {@link #getRequiredPermissions()}.
+ *
+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility for
+ * supported Permissions.
+ */
+ @SuppressWarnings("RequiresPermission") // No permission required to call this method
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addRequiredPermissions(@NonNull Set<Integer> visibleToPermissions) {
+ Objects.requireNonNull(visibleToPermissions);
+ resetIfBuilt();
+ mRequiredPermissions.add(new VisibilityPermissionConfig(visibleToPermissions));
+ return this;
+ }
+
+ /**
+ * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearRequiredPermissions() {
+ resetIfBuilt();
+ mRequiredPermissions.clear();
+ return this;
+ }
+
+ /**
+ * Specify that this schema should be publicly available, to the same packages that have
+ * visibility to the package passed as a parameter. This visibility is determined by the
+ * result of {@link android.content.pm.PackageManager#canPackageQuery}.
+ *
+ * <p> It is possible for the packageIdentifier parameter to be different from the
+ * package performing the indexing. This might happen in the case of an on-device indexer
+ * processing information about various packages. The visibility will be the same
+ * regardless of which package indexes the document, as the visibility is based on the
+ * packageIdentifier parameter.
+ *
+ * <p> Calling this with packageIdentifier set to null is valid, and will remove public
+ * visibility for the schema.
+ *
+ * @param packageIdentifier the {@link PackageIdentifier} of the package that will be used
+ * as the target package in a call to {@link
+ * android.content.pm.PackageManager#canPackageQuery} to determine
+ * which packages can access this publicly visible schema.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setPubliclyVisibleTargetPackage(
+ @Nullable PackageIdentifier packageIdentifier) {
+ resetIfBuilt();
+ if (packageIdentifier == null) {
+ mPubliclyVisibleTargetPackage = null;
+ } else {
+ mPubliclyVisibleTargetPackage = packageIdentifier.getPackageIdentifierParcel();
+ }
+ return this;
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mAllowedPackages = new ArrayList<>(mAllowedPackages);
+ mRequiredPermissions = new ArrayList<>(mRequiredPermissions);
+ mBuilt = false;
+ }
+ }
+
+ /** Build a {@link SchemaVisibilityConfig} */
+ @NonNull
+ public SchemaVisibilityConfig build() {
+ mBuilt = true;
+ return new SchemaVisibilityConfig(
+ mAllowedPackages,
+ mRequiredPermissions,
+ mPubliclyVisibleTargetPackage);
+ }
+ }
+}
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 187304f..0fce482 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -16,7 +16,8 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,10 +25,18 @@
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator;
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;
@@ -46,36 +55,61 @@
*
* @see SearchResults
*/
-public final class SearchResult {
- static final String DOCUMENT_FIELD = "document";
- static final String MATCH_INFOS_FIELD = "matchInfos";
- static final String PACKAGE_NAME_FIELD = "packageName";
- static final String DATABASE_NAME_FIELD = "databaseName";
- static final String RANKING_SIGNAL_FIELD = "rankingSignal";
- static final String JOINED_RESULTS = "joinedResults";
[email protected](creator = "SearchResultCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchResult extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<SearchResult> CREATOR =
+ new SearchResultCreator();
+ @Field(id = 1)
+ final GenericDocumentParcel mDocument;
+ @Field(id = 2)
+ final List<MatchInfo> mMatchInfos;
+ @Field(id = 3, getter = "getPackageName")
+ private final String mPackageName;
+ @Field(id = 4, getter = "getDatabaseName")
+ private final String mDatabaseName;
+ @Field(id = 5, getter = "getRankingSignal")
+ private final double mRankingSignal;
+ @Field(id = 6, getter = "getJoinedResults")
+ private final List<SearchResult> mJoinedResults;
@NonNull
- private final Bundle mBundle;
+ @Field(id = 7, getter = "getInformationalRankingSignals")
+ private final List<Double> mInformationalRankingSignals;
- /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */
- @Nullable
- private GenericDocument mDocument;
- /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
+ /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
@Nullable
- private List<MatchInfo> mMatchInfos;
+ private GenericDocument mDocumentCached;
+
+ /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */
+ @Nullable
+ private List<MatchInfo> mMatchInfosCached;
/** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public SearchResult(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
-
- /** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
+ @Constructor
+ SearchResult(
+ @Param(id = 1) @NonNull GenericDocumentParcel document,
+ @Param(id = 2) @NonNull List<MatchInfo> matchInfos,
+ @Param(id = 3) @NonNull String packageName,
+ @Param(id = 4) @NonNull String databaseName,
+ @Param(id = 5) double rankingSignal,
+ @Param(id = 6) @NonNull List<SearchResult> joinedResults,
+ @Param(id = 7) @Nullable List<Double> informationalRankingSignals) {
+ mDocument = Preconditions.checkNotNull(document);
+ mMatchInfos = Preconditions.checkNotNull(matchInfos);
+ mPackageName = Preconditions.checkNotNull(packageName);
+ mDatabaseName = Preconditions.checkNotNull(databaseName);
+ mRankingSignal = rankingSignal;
+ mJoinedResults = Collections.unmodifiableList(Preconditions.checkNotNull(joinedResults));
+ if (informationalRankingSignals != null) {
+ mInformationalRankingSignals = Collections.unmodifiableList(
+ informationalRankingSignals);
+ } else {
+ mInformationalRankingSignals = Collections.emptyList();
+ }
}
// @exportToFramework:startStrip()
@@ -92,7 +126,7 @@
* @see GenericDocument#toDocumentClass(Class)
*/
@NonNull
- public <T> T getDocument(@NonNull Class<T> documentClass) throws AppSearchException {
+ public <T> T getDocument(@NonNull java.lang.Class<T> documentClass) throws AppSearchException {
return getDocument(documentClass, /* documentClassMap= */null);
}
@@ -112,7 +146,7 @@
* @see GenericDocument#toDocumentClass(Class, Map)
*/
@NonNull
- public <T> T getDocument(@NonNull Class<T> documentClass,
+ public <T> T getDocument(@NonNull java.lang.Class<T> documentClass,
@Nullable Map<String, List<String>> documentClassMap) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
return getGenericDocument().toDocumentClass(documentClass, documentClassMap);
@@ -126,11 +160,10 @@
*/
@NonNull
public GenericDocument getGenericDocument() {
- if (mDocument == null) {
- mDocument = new GenericDocument(
- Preconditions.checkNotNull(mBundle.getBundle(DOCUMENT_FIELD)));
+ if (mDocumentCached == null) {
+ mDocumentCached = new GenericDocument(mDocument);
}
- return mDocument;
+ return mDocumentCached;
}
/**
@@ -143,22 +176,21 @@
* value, this method returns an empty list.
*/
@NonNull
- @SuppressWarnings("deprecation")
public List<MatchInfo> getMatchInfos() {
- if (mMatchInfos == null) {
- List<Bundle> matchBundles =
- Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
- mMatchInfos = new ArrayList<>(matchBundles.size());
- for (int i = 0; i < matchBundles.size(); i++) {
- MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
- if (mMatchInfos != null) {
+ if (mMatchInfosCached == null) {
+ mMatchInfosCached = new ArrayList<>(mMatchInfos.size());
+ for (int i = 0; i < mMatchInfos.size(); i++) {
+ MatchInfo matchInfo = mMatchInfos.get(i);
+ matchInfo.setDocument(getGenericDocument());
+ if (mMatchInfosCached != null) {
// This additional check is added for NullnessChecker.
- mMatchInfos.add(matchInfo);
+ mMatchInfosCached.add(matchInfo);
}
}
+ mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached);
}
// This check is added for NullnessChecker, mMatchInfos will always be NonNull.
- return Preconditions.checkNotNull(mMatchInfos);
+ return Preconditions.checkNotNull(mMatchInfosCached);
}
/**
@@ -168,7 +200,7 @@
*/
@NonNull
public String getPackageName() {
- return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
+ return mPackageName;
}
/**
@@ -178,7 +210,7 @@
*/
@NonNull
public String getDatabaseName() {
- return Preconditions.checkNotNull(mBundle.getString(DATABASE_NAME_FIELD));
+ return mDatabaseName;
}
/**
@@ -207,7 +239,17 @@
* @return Ranking signal of the document
*/
public double getRankingSignal() {
- return mBundle.getDouble(RANKING_SIGNAL_FIELD);
+ return mRankingSignal;
+ }
+
+ /**
+ * Returns the informational ranking signals of the {@link GenericDocument}, according to the
+ * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}.
+ */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public List<Double> getInformationalRankingSignals() {
+ return mInformationalRankingSignals;
}
/**
@@ -225,28 +267,26 @@
* @return a List of SearchResults containing joined documents.
*/
@NonNull
- @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated.
public List<SearchResult> getJoinedResults() {
- ArrayList<Bundle> bundles = mBundle.getParcelableArrayList(JOINED_RESULTS);
- if (bundles == null) {
- return new ArrayList<>();
- }
- List<SearchResult> res = new ArrayList<>(bundles.size());
- for (int i = 0; i < bundles.size(); i++) {
- res.add(new SearchResult(bundles.get(i)));
- }
+ return mJoinedResults;
+ }
- return res;
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SearchResultCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link SearchResult} objects. */
public static final class Builder {
private final String mPackageName;
private final String mDatabaseName;
- private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>();
+ private List<MatchInfo> mMatchInfos = new ArrayList<>();
private GenericDocument mGenericDocument;
private double mRankingSignal;
- private ArrayList<Bundle> mJoinedResults = new ArrayList<>();
+ private List<Double> mInformationalRankingSignals = new ArrayList<>();
+ private List<SearchResult> mJoinedResults = new ArrayList<>();
private boolean mBuilt = false;
/**
@@ -260,6 +300,26 @@
mDatabaseName = Preconditions.checkNotNull(databaseName);
}
+ /** @exportToFramework:hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull SearchResult searchResult) {
+ Preconditions.checkNotNull(searchResult);
+ mPackageName = searchResult.getPackageName();
+ mDatabaseName = searchResult.getDatabaseName();
+ mGenericDocument = searchResult.getGenericDocument();
+ mRankingSignal = searchResult.getRankingSignal();
+ mInformationalRankingSignals = new ArrayList<>(
+ searchResult.getInformationalRankingSignals());
+ List<MatchInfo> matchInfos = searchResult.getMatchInfos();
+ for (int i = 0; i < matchInfos.size(); i++) {
+ addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
+ }
+ List<SearchResult> joinedResults = searchResult.getJoinedResults();
+ for (int i = 0; i < joinedResults.size(); i++) {
+ addJoinedResult(joinedResults.get(i));
+ }
+ }
+
// @exportToFramework:startStrip()
/**
* Sets the document which matched.
@@ -298,7 +358,7 @@
"This MatchInfo is already associated with a SearchResult and can't be "
+ "reassigned");
resetIfBuilt();
- mMatchInfoBundles.add(matchInfo.mBundle);
+ mMatchInfos.add(matchInfo);
return this;
}
@@ -311,6 +371,17 @@
return this;
}
+ /** Adds the informational ranking signal of the matched document in this SearchResult. */
+ @CanIgnoreReturnValue
+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ @NonNull
+ public Builder addInformationalRankingSignal(double rankingSignal) {
+ resetIfBuilt();
+ mInformationalRankingSignals.add(rankingSignal);
+ return this;
+ }
+
+
/**
* Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
* @param joinedResult The joined SearchResult to add.
@@ -319,28 +390,43 @@
@NonNull
public Builder addJoinedResult(@NonNull SearchResult joinedResult) {
resetIfBuilt();
- mJoinedResults.add(joinedResult.getBundle());
+ mJoinedResults.add(joinedResult);
+ return this;
+ }
+
+ /**
+ * Clears the {@link SearchResult}s that were joined.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearJoinedResults() {
+ resetIfBuilt();
+ mJoinedResults.clear();
return this;
}
/** Constructs a new {@link SearchResult}. */
@NonNull
public SearchResult build() {
- Bundle bundle = new Bundle();
- bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
- bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
- bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
- bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
- bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
- bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults);
mBuilt = true;
- return new SearchResult(bundle);
+ return new SearchResult(
+ mGenericDocument.getDocumentParcel(),
+ mMatchInfos,
+ mPackageName,
+ mDatabaseName,
+ mRankingSignal,
+ mJoinedResults,
+ mInformationalRankingSignals);
}
private void resetIfBuilt() {
if (mBuilt) {
- mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
+ mMatchInfos = new ArrayList<>(mMatchInfos);
mJoinedResults = new ArrayList<>(mJoinedResults);
+ mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
mBuilt = false;
}
}
@@ -410,20 +496,32 @@
* <li>{@link MatchInfo#getSnippet()} returns "Testing 1"</li>
* </ul>
*/
- public static final class MatchInfo {
- /** The path of the matching snippet property. */
- private static final String PROPERTY_PATH_FIELD = "propertyPath";
- private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
- private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
- private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower";
- private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper";
- private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
- private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
+ @SafeParcelable.Class(creator = "MatchInfoCreator")
+ @SuppressWarnings("HiddenSuperclass")
+ public static final class MatchInfo extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<MatchInfo> CREATOR =
+ new MatchInfoCreator();
+ /** The path of the matching snippet property. */
+ @Field(id = 1, getter = "getPropertyPath")
private final String mPropertyPath;
+ @Field(id = 2)
+ final int mExactMatchRangeStart;
+ @Field(id = 3)
+ final int mExactMatchRangeEnd;
+ @Field(id = 4)
+ final int mSubmatchRangeStart;
+ @Field(id = 5)
+ final int mSubmatchRangeEnd;
+ @Field(id = 6)
+ final int mSnippetRangeStart;
+ @Field(id = 7)
+ final int mSnippetRangeEnd;
+
@Nullable
private PropertyPath mPropertyPathObject = null;
- final Bundle mBundle;
/**
* Document which the match comes from.
@@ -432,7 +530,7 @@
* {@link #getExactMatch}, will throw {@link NullPointerException}.
*/
@Nullable
- final GenericDocument mDocument;
+ private GenericDocument mDocument = null;
/** Full text of the matched property. Populated on first use. */
@Nullable
@@ -440,23 +538,35 @@
/** Range of property that exactly matched the query. Populated on first use. */
@Nullable
- private MatchRange mExactMatchRange;
+ private MatchRange mExactMatchRangeCached;
/**
* Range of property that corresponds to the subsequence of the exact match that directly
* matches a query term. Populated on first use.
*/
@Nullable
- private MatchRange mSubmatchRange;
+ private MatchRange mSubmatchRangeCached;
/** Range of some reasonable amount of context around the query. Populated on first use. */
@Nullable
- private MatchRange mWindowRange;
+ private MatchRange mWindowRangeCached;
- MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
- mBundle = Preconditions.checkNotNull(bundle);
- mDocument = document;
- mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD));
+ @Constructor
+ MatchInfo(
+ @Param(id = 1) @NonNull String propertyPath,
+ @Param(id = 2) int exactMatchRangeStart,
+ @Param(id = 3) int exactMatchRangeEnd,
+ @Param(id = 4) int submatchRangeStart,
+ @Param(id = 5) int submatchRangeEnd,
+ @Param(id = 6) int snippetRangeStart,
+ @Param(id = 7) int snippetRangeEnd) {
+ mPropertyPath = Preconditions.checkNotNull(propertyPath);
+ mExactMatchRangeStart = exactMatchRangeStart;
+ mExactMatchRangeEnd = exactMatchRangeEnd;
+ mSubmatchRangeStart = submatchRangeStart;
+ mSubmatchRangeEnd = submatchRangeEnd;
+ mSnippetRangeStart = snippetRangeStart;
+ mSnippetRangeEnd = snippetRangeEnd;
}
/**
@@ -521,12 +631,12 @@
*/
@NonNull
public MatchRange getExactMatchRange() {
- if (mExactMatchRange == null) {
- mExactMatchRange = new MatchRange(
- mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
- mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
+ if (mExactMatchRangeCached == null) {
+ mExactMatchRangeCached = new MatchRange(
+ mExactMatchRangeStart,
+ mExactMatchRangeEnd);
}
- return mExactMatchRange;
+ return mExactMatchRangeCached;
}
/**
@@ -555,20 +665,18 @@
* false.
* <!--@exportToFramework:else()-->
*/
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
- // @exportToFramework:endStrip()
@NonNull
public MatchRange getSubmatchRange() {
checkSubmatchSupported();
- if (mSubmatchRange == null) {
- mSubmatchRange = new MatchRange(
- mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD),
- mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD));
+ if (mSubmatchRangeCached == null) {
+ mSubmatchRangeCached = new MatchRange(
+ mSubmatchRangeStart,
+ mSubmatchRangeEnd);
}
- return mSubmatchRange;
+ return mSubmatchRangeCached;
}
/**
@@ -585,11 +693,9 @@
* false.
* <!--@exportToFramework:else()-->
*/
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
- // @exportToFramework:endStrip()
@NonNull
public CharSequence getSubmatch() {
checkSubmatchSupported();
@@ -606,12 +712,12 @@
*/
@NonNull
public MatchRange getSnippetRange() {
- if (mWindowRange == null) {
- mWindowRange = new MatchRange(
- mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
- mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
+ if (mWindowRangeCached == null) {
+ mWindowRangeCached = new MatchRange(
+ mSnippetRangeStart,
+ mSnippetRangeEnd);
}
- return mWindowRange;
+ return mWindowRangeCached;
}
/**
@@ -634,7 +740,7 @@
}
private void checkSubmatchSupported() {
- if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) {
+ if (mSubmatchRangeStart == -1) {
throw new UnsupportedOperationException(
"Submatch is not supported with this backend/Android API level "
+ "combination");
@@ -651,11 +757,29 @@
return result;
}
+ /**
+ * Sets the {@link GenericDocument} for {@link MatchInfo}.
+ *
+ * {@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument}
+ * This provides the ability to set {@link MatchInfo#mDocument}
+ */
+ void setDocument(@NonNull GenericDocument document) {
+ mDocument = document;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ MatchInfoCreator.writeToParcel(this, dest, flags);
+ }
+
/** Builder for {@link MatchInfo} objects. */
public static final class Builder {
private final String mPropertyPath;
private MatchRange mExactMatchRange = new MatchRange(0, 0);
- @Nullable private MatchRange mSubmatchRange;
+ int mSubmatchRangeStart = -1;
+ int mSubmatchRangeEnd = -1;
private MatchRange mSnippetRange = new MatchRange(0, 0);
/**
@@ -675,6 +799,17 @@
mPropertyPath = Preconditions.checkNotNull(propertyPath);
}
+ /** @exportToFramework:hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull MatchInfo matchInfo) {
+ Preconditions.checkNotNull(matchInfo);
+ mPropertyPath = matchInfo.mPropertyPath;
+ mExactMatchRange = matchInfo.getExactMatchRange();
+ mSubmatchRangeStart = matchInfo.mSubmatchRangeStart;
+ mSubmatchRangeEnd = matchInfo.mSubmatchRangeEnd;
+ mSnippetRange = matchInfo.getSnippetRange();
+ }
+
/** Sets the exact {@link MatchRange} corresponding to the given entry. */
@CanIgnoreReturnValue
@NonNull
@@ -684,11 +819,15 @@
}
- /** Sets the submatch {@link MatchRange} corresponding to the given entry. */
+ /**
+ * Sets the start and end of a submatch {@link MatchRange} corresponding
+ * to the given entry.
+ */
@CanIgnoreReturnValue
@NonNull
public Builder setSubmatchRange(@NonNull MatchRange matchRange) {
- mSubmatchRange = Preconditions.checkNotNull(matchRange);
+ mSubmatchRangeStart = matchRange.getStart();
+ mSubmatchRangeEnd = matchRange.getEnd();
return this;
}
@@ -703,24 +842,14 @@
/** Constructs a new {@link MatchInfo}. */
@NonNull
public MatchInfo build() {
- Bundle bundle = new Bundle();
- bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath);
- bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart());
- bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd());
- if (mSubmatchRange != null) {
- // Only populate the submatch fields if it was actually set.
- bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart());
- }
-
- if (mSubmatchRange != null) {
- // Only populate the submatch fields if it was actually set.
- // Moved to separate block for Nullness Checker.
- bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd());
- }
-
- bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart());
- bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd());
- return new MatchInfo(bundle, /*document=*/ null);
+ return new MatchInfo(
+ mPropertyPath,
+ mExactMatchRange.getStart(),
+ mExactMatchRange.getEnd(),
+ mSubmatchRangeStart,
+ mSubmatchRangeEnd,
+ mSnippetRange.getStart(),
+ mSnippetRange.getEnd());
}
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
index 568c2b6..d5d17a3 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
@@ -16,14 +16,16 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultPageCreator;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -32,26 +34,29 @@
* @exportToFramework:hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SearchResultPage {
- public static final String RESULTS_FIELD = "results";
- public static final String NEXT_PAGE_TOKEN_FIELD = "nextPageToken";
[email protected](creator = "SearchResultPageCreator")
+public class SearchResultPage extends AbstractSafeParcelable {
+ @NonNull public static final Parcelable.Creator<SearchResultPage> CREATOR =
+ new SearchResultPageCreator();
+
+ @Field(id = 1, getter = "getNextPageToken")
private final long mNextPageToken;
-
@Nullable
- private List<SearchResult> mResults;
+ @Field(id = 2, getter = "getResults")
+ private final List<SearchResult> mResults;
- @NonNull
- private final Bundle mBundle;
-
- public SearchResultPage(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- mNextPageToken = mBundle.getLong(NEXT_PAGE_TOKEN_FIELD);
+ @Constructor
+ public SearchResultPage(
+ @Param(id = 1) long nextPageToken,
+ @Param(id = 2) @Nullable List<SearchResult> results) {
+ mNextPageToken = nextPageToken;
+ mResults = results;
}
- /** Returns the {@link Bundle} of this class. */
- @NonNull
- public Bundle getBundle() {
- return mBundle;
+ /** Default constructor for {@link SearchResultPage}. */
+ public SearchResultPage() {
+ mNextPageToken = 0;
+ mResults = Collections.emptyList();
}
/** Returns the Token to get next {@link SearchResultPage}. */
@@ -61,19 +66,15 @@
/** Returns all {@link androidx.appsearch.app.SearchResult}s of this page */
@NonNull
- @SuppressWarnings("deprecation")
public List<SearchResult> getResults() {
if (mResults == null) {
- ArrayList<Bundle> resultBundles = mBundle.getParcelableArrayList(RESULTS_FIELD);
- if (resultBundles == null) {
- mResults = Collections.emptyList();
- } else {
- mResults = new ArrayList<>(resultBundles.size());
- for (int i = 0; i < resultBundles.size(); i++) {
- mResults.add(new SearchResult(resultBundles.get(i)));
- }
- }
+ return Collections.emptyList();
}
return mResults;
}
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SearchResultPageCreator.writeToParcel(this, dest, flags);
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 9b86fc3..1b43e15 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint;
import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
@@ -28,6 +30,11 @@
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSpecCreator;
import androidx.appsearch.util.BundleUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
@@ -41,49 +48,119 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
* This class represents the specification logic for AppSearch. It can be used to set the type of
* search, like prefix or exact only or apply filters to search for a specific schema type only etc.
*/
-public final class SearchSpec {
[email protected](creator = "SearchSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSpec extends AbstractSafeParcelable {
+
+ /** Creator class for {@link SearchSpec}. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<SearchSpec> CREATOR =
+ new SearchSpecCreator();
+
/**
* Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply
* property paths to all results, excepting any types that have had their own, specific
* property paths set.
+ *
+ * @deprecated use {@link #SCHEMA_TYPE_WILDCARD} instead.
*/
+ @Deprecated
public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
/**
* Schema type to be used in {@link SearchSpec.Builder#addFilterProperties(String, Collection)}
- * to apply property paths to all results, excepting any types that have had their own, specific
- * property paths set.
- * @exportToFramework:hide
+ * and {@link SearchSpec.Builder#addProjection} to apply property paths to all results,
+ * excepting any types that have had their own, specific property paths set.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public static final String SCHEMA_TYPE_WILDCARD = "*";
- static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
- static final String SCHEMA_FIELD = "schema";
- static final String NAMESPACE_FIELD = "namespace";
- static final String PROPERTY_FIELD = "property";
- static final String PACKAGE_NAME_FIELD = "packageName";
- static final String NUM_PER_PAGE_FIELD = "numPerPage";
- static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
- static final String ORDER_FIELD = "order";
- static final String SNIPPET_COUNT_FIELD = "snippetCount";
- static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
- static final String MAX_SNIPPET_FIELD = "maxSnippet";
- static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
- static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
- static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
- static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";
- static final String JOIN_SPEC = "joinSpec";
- static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression";
- static final String ENABLED_FEATURES_FIELD = "enabledFeatures";
+ @Field(id = 1, getter = "getTermMatch")
+ private final int mTermMatchType;
- /** @exportToFramework:hide */
+ @Field(id = 2, getter = "getFilterSchemas")
+ private final List<String> mSchemas;
+
+ @Field(id = 3, getter = "getFilterNamespaces")
+ private final List<String> mNamespaces;
+
+ @Field(id = 4)
+ final Bundle mTypePropertyFilters;
+
+ @Field(id = 5, getter = "getFilterPackageNames")
+ private final List<String> mPackageNames;
+
+ @Field(id = 6, getter = "getResultCountPerPage")
+ private final int mResultCountPerPage;
+
+ @Field(id = 7, getter = "getRankingStrategy")
+ @RankingStrategy
+ private final int mRankingStrategy;
+
+ @Field(id = 8, getter = "getOrder")
+ @Order
+ private final int mOrder;
+
+ @Field(id = 9, getter = "getSnippetCount")
+ private final int mSnippetCount;
+
+ @Field(id = 10, getter = "getSnippetCountPerProperty")
+ private final int mSnippetCountPerProperty;
+
+ @Field(id = 11, getter = "getMaxSnippetSize")
+ private final int mMaxSnippetSize;
+
+ @Field(id = 12)
+ final Bundle mProjectionTypePropertyMasks;
+
+ @Field(id = 13, getter = "getResultGroupingTypeFlags")
+ @GroupingType
+ private final int mResultGroupingTypeFlags;
+
+ @Field(id = 14, getter = "getResultGroupingLimit")
+ private final int mGroupingLimit;
+
+ @Field(id = 15)
+ final Bundle mTypePropertyWeightsField;
+
+ @Nullable
+ @Field(id = 16, getter = "getJoinSpec")
+ private final JoinSpec mJoinSpec;
+
+ @Field(id = 17, getter = "getAdvancedRankingExpression")
+ private final String mAdvancedRankingExpression;
+
+ @Field(id = 18, getter = "getEnabledFeatures")
+ private final List<String> mEnabledFeatures;
+
+ @Field(id = 19, getter = "getSearchSourceLogTag")
+ @Nullable private final String mSearchSourceLogTag;
+
+ @NonNull
+ @Field(id = 20, getter = "getSearchEmbeddings")
+ private final List<EmbeddingVector> mSearchEmbeddings;
+
+ @Field(id = 21, getter = "getDefaultEmbeddingSearchMetricType")
+ private final int mDefaultEmbeddingSearchMetricType;
+
+ @NonNull
+ @Field(id = 22, getter = "getInformationalRankingExpressions")
+ private final List<String> mInformationalRankingExpressions;
+
+ /**
+ * Default number of documents per page.
+ *
+ * @exportToFramework:hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int DEFAULT_NUM_PER_PAGE = 10;
@@ -224,43 +301,112 @@
/**
* Results should be grouped together by schema type for the purpose of enforcing a limit on the
* number of results returned per schema type.
- *
- * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
- * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
- * Mainline are possible.
- * -->
*/
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)
- // @exportToFramework:endStrip()
+ @FlaggedApi(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA)
public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
- private final Bundle mBundle;
-
- /** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public SearchSpec(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
- }
-
/**
- * Returns the {@link Bundle} populated by this builder.
+ * Type of scoring used to calculate similarity for embedding vectors. For details of each, see
+ * comments above each value.
*
* @exportToFramework:hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link SearchSpecProto.EmbeddingQueryMetricType.Code}
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef(value = {
+ EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+ EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT,
+ EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EmbeddingSearchMetricType {
}
+ /**
+ * Cosine similarity as metric for embedding search and ranking.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1;
+ /**
+ * Dot product similarity as metric for embedding search and ranking.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2;
+ /**
+ * Euclidean distance as metric for embedding search and ranking.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3;
+
+
+ @Constructor
+ SearchSpec(
+ @Param(id = 1) int termMatchType,
+ @Param(id = 2) @NonNull List<String> schemas,
+ @Param(id = 3) @NonNull List<String> namespaces,
+ @Param(id = 4) @NonNull Bundle properties,
+ @Param(id = 5) @NonNull List<String> packageNames,
+ @Param(id = 6) int resultCountPerPage,
+ @Param(id = 7) @RankingStrategy int rankingStrategy,
+ @Param(id = 8) @Order int order,
+ @Param(id = 9) int snippetCount,
+ @Param(id = 10) int snippetCountPerProperty,
+ @Param(id = 11) int maxSnippetSize,
+ @Param(id = 12) @NonNull Bundle projectionTypePropertyMasks,
+ @Param(id = 13) int resultGroupingTypeFlags,
+ @Param(id = 14) int groupingLimit,
+ @Param(id = 15) @NonNull Bundle typePropertyWeightsField,
+ @Param(id = 16) @Nullable JoinSpec joinSpec,
+ @Param(id = 17) @NonNull String advancedRankingExpression,
+ @Param(id = 18) @NonNull List<String> enabledFeatures,
+ @Param(id = 19) @Nullable String searchSourceLogTag,
+ @Param(id = 20) @Nullable List<EmbeddingVector> searchEmbeddings,
+ @Param(id = 21) int defaultEmbeddingSearchMetricType,
+ @Param(id = 22) @Nullable List<String> informationalRankingExpressions
+ ) {
+ mTermMatchType = termMatchType;
+ mSchemas = Collections.unmodifiableList(Preconditions.checkNotNull(schemas));
+ mNamespaces = Collections.unmodifiableList(Preconditions.checkNotNull(namespaces));
+ mTypePropertyFilters = Preconditions.checkNotNull(properties);
+ mPackageNames = Collections.unmodifiableList(Preconditions.checkNotNull(packageNames));
+ mResultCountPerPage = resultCountPerPage;
+ mRankingStrategy = rankingStrategy;
+ mOrder = order;
+ mSnippetCount = snippetCount;
+ mSnippetCountPerProperty = snippetCountPerProperty;
+ mMaxSnippetSize = maxSnippetSize;
+ mProjectionTypePropertyMasks = Preconditions.checkNotNull(projectionTypePropertyMasks);
+ mResultGroupingTypeFlags = resultGroupingTypeFlags;
+ mGroupingLimit = groupingLimit;
+ mTypePropertyWeightsField = Preconditions.checkNotNull(typePropertyWeightsField);
+ mJoinSpec = joinSpec;
+ mAdvancedRankingExpression = Preconditions.checkNotNull(advancedRankingExpression);
+ mEnabledFeatures = Collections.unmodifiableList(
+ Preconditions.checkNotNull(enabledFeatures));
+ mSearchSourceLogTag = searchSourceLogTag;
+ if (searchEmbeddings != null) {
+ mSearchEmbeddings = Collections.unmodifiableList(searchEmbeddings);
+ } else {
+ mSearchEmbeddings = Collections.emptyList();
+ }
+ mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+ if (informationalRankingExpressions != null) {
+ mInformationalRankingExpressions = Collections.unmodifiableList(
+ informationalRankingExpressions);
+ } else {
+ mInformationalRankingExpressions = Collections.emptyList();
+ }
+ }
+
+
/** Returns how the query terms should match terms in the index. */
@TermMatch
public int getTermMatch() {
- return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
+ return mTermMatchType;
}
/**
@@ -270,11 +416,10 @@
*/
@NonNull
public List<String> getFilterSchemas() {
- List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
- if (schemas == null) {
+ if (mSchemas == null) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(schemas);
+ return mSchemas;
}
/**
@@ -284,19 +429,15 @@
*
* <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.
- *
- * @exportToFramework:hide
*/
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Map<String, List<String>> getFilterProperties() {
- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
- mBundle.getBundle(PROPERTY_FIELD));
- Set<String> schemas = typePropertyPathsBundle.keySet();
+ Set<String> schemas = mTypePropertyFilters.keySet();
Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
for (String schema : schemas) {
typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
- typePropertyPathsBundle.getStringArrayList(schema)));
+ mTypePropertyFilters.getStringArrayList(schema)));
}
return typePropertyPathsMap;
}
@@ -308,11 +449,10 @@
*/
@NonNull
public List<String> getFilterNamespaces() {
- List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
- if (namespaces == null) {
+ if (mNamespaces == null) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(namespaces);
+ return mNamespaces;
}
/**
@@ -324,45 +464,44 @@
*/
@NonNull
public List<String> getFilterPackageNames() {
- List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
- if (packageNames == null) {
+ if (mPackageNames == null) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(packageNames);
+ return mPackageNames;
}
/** Returns the number of results per page in the result set. */
public int getResultCountPerPage() {
- return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
+ return mResultCountPerPage;
}
/** Returns the ranking strategy. */
@RankingStrategy
public int getRankingStrategy() {
- return mBundle.getInt(RANKING_STRATEGY_FIELD);
+ return mRankingStrategy;
}
/** Returns the order of returned search results (descending or ascending). */
@Order
public int getOrder() {
- return mBundle.getInt(ORDER_FIELD);
+ return mOrder;
}
/** Returns how many documents to generate snippets for. */
public int getSnippetCount() {
- return mBundle.getInt(SNIPPET_COUNT_FIELD);
+ return mSnippetCount;
}
/**
* Returns how many matches for each property of a matching document to generate snippets for.
*/
public int getSnippetCountPerProperty() {
- return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);
+ return mSnippetCountPerProperty;
}
/** Returns the maximum size of a snippet in characters. */
public int getMaxSnippetSize() {
- return mBundle.getInt(MAX_SNIPPET_FIELD);
+ return mMaxSnippetSize;
}
/**
@@ -377,13 +516,12 @@
*/
@NonNull
public Map<String, List<String>> getProjections() {
- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
- mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD));
- Set<String> schemas = typePropertyPathsBundle.keySet();
+ Set<String> schemas = mProjectionTypePropertyMasks.keySet();
Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
for (String schema : schemas) {
- typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
- typePropertyPathsBundle.getStringArrayList(schema)));
+ typePropertyPathsMap.put(schema,
+ Objects.requireNonNull(
+ mProjectionTypePropertyMasks.getStringArrayList(schema)));
}
return typePropertyPathsMap;
}
@@ -400,16 +538,19 @@
*/
@NonNull
public Map<String, List<PropertyPath>> getProjectionPaths() {
- Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
- Set<String> schemas = typePropertyPathsBundle.keySet();
+ Set<String> schemas = mProjectionTypePropertyMasks.keySet();
Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
for (String schema : schemas) {
- ArrayList<String> propertyPathList = typePropertyPathsBundle.getStringArrayList(schema);
- List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
- for (String p: propertyPathList) {
- copy.add(new PropertyPath(p));
+ ArrayList<String> propertyPathList = mProjectionTypePropertyMasks.getStringArrayList(
+ schema);
+ if (propertyPathList != null) {
+ List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
+ for (int i = 0; i < propertyPathList.size(); i++) {
+ String p = propertyPathList.get(i);
+ copy.add(new PropertyPath(p));
+ }
+ typePropertyPathsMap.put(schema, copy);
}
- typePropertyPathsMap.put(schema, copy);
}
return typePropertyPathsMap;
}
@@ -425,18 +566,20 @@
*/
@NonNull
public Map<String, Map<String, Double>> getPropertyWeights() {
- Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
- Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+ Set<String> schemaTypes = mTypePropertyWeightsField.keySet();
Map<String, Map<String, Double>> typePropertyWeightsMap = new ArrayMap<>(
schemaTypes.size());
for (String schemaType : schemaTypes) {
- Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
- Set<String> propertyPaths = propertyPathBundle.keySet();
- Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
- for (String propertyPath : propertyPaths) {
- propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));
+ Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
+ if (propertyPathBundle != null) {
+ Set<String> propertyPaths = propertyPathBundle.keySet();
+ Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ propertyPathWeights.put(propertyPath,
+ propertyPathBundle.getDouble(propertyPath));
+ }
+ typePropertyWeightsMap.put(schemaType, propertyPathWeights);
}
- typePropertyWeightsMap.put(schemaType, propertyPathWeights);
}
return typePropertyWeightsMap;
}
@@ -452,19 +595,22 @@
*/
@NonNull
public Map<String, Map<PropertyPath, Double>> getPropertyWeightPaths() {
- Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
- Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+ Set<String> schemaTypes = mTypePropertyWeightsField.keySet();
Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap = new ArrayMap<>(
schemaTypes.size());
for (String schemaType : schemaTypes) {
- Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
- Set<String> propertyPaths = propertyPathBundle.keySet();
- Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
- for (String propertyPath : propertyPaths) {
- propertyPathWeights.put(new PropertyPath(propertyPath),
- propertyPathBundle.getDouble(propertyPath));
+ Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
+ if (propertyPathBundle != null) {
+ Set<String> propertyPaths = propertyPathBundle.keySet();
+ Map<PropertyPath, Double> propertyPathWeights =
+ new ArrayMap<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ propertyPathWeights.put(
+ new PropertyPath(propertyPath),
+ propertyPathBundle.getDouble(propertyPath));
+ }
+ typePropertyWeightsMap.put(schemaType, propertyPathWeights);
}
- typePropertyWeightsMap.put(schemaType, propertyPathWeights);
}
return typePropertyWeightsMap;
}
@@ -475,7 +621,7 @@
*/
@GroupingType
public int getResultGroupingTypeFlags() {
- return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
+ return mResultGroupingTypeFlags;
}
/**
@@ -485,7 +631,7 @@
* {@link Builder#setResultGrouping(int, int)} was not called.
*/
public int getResultGroupingLimit() {
- return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
+ return mGroupingLimit;
}
/**
@@ -493,11 +639,7 @@
*/
@Nullable
public JoinSpec getJoinSpec() {
- Bundle joinSpec = mBundle.getBundle(JOIN_SPEC);
- if (joinSpec == null) {
- return null;
- }
- return new JoinSpec(joinSpec);
+ return mJoinSpec;
}
/**
@@ -506,28 +648,104 @@
*/
@NonNull
public String getAdvancedRankingExpression() {
- return mBundle.getString(ADVANCED_RANKING_EXPRESSION, "");
+ return mAdvancedRankingExpression;
+ }
+
+
+ /**
+ * Gets a tag to indicate the source of this search, or {@code null} if
+ * {@link Builder#setSearchSourceLogTag(String)} was not called.
+ *
+ * <p> Some AppSearch implementations may log a hash of this tag using statsd. This tag may be
+ * used for tracing performance issues and crashes to a component of an app.
+ *
+ * <p>Call {@link Builder#setSearchSourceLogTag} and give a unique value if you want to
+ * distinguish this search scenario with other search scenarios during performance analysis.
+ *
+ * <p>Under no circumstances will AppSearch log the raw String value using statsd, but it
+ * will be provided as-is to custom {@code AppSearchLogger} implementations you have
+ * registered in your app.
+ */
+ @Nullable
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+ public String getSearchSourceLogTag() {
+ return mSearchSourceLogTag;
+ }
+
+ /**
+ * Returns the list of {@link EmbeddingVector} for embedding search.
+ */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public List<EmbeddingVector> getSearchEmbeddings() {
+ return mSearchEmbeddings;
+ }
+
+ /**
+ * Returns the default embedding metric type used for embedding search
+ * (see {@link AppSearchSession#search}) and ranking
+ * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).
+ */
+ @EmbeddingSearchMetricType
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public int getDefaultEmbeddingSearchMetricType() {
+ return mDefaultEmbeddingSearchMetricType;
+ }
+
+ /**
+ * Returns the informational ranking expressions.
+ *
+ * @see Builder#addInformationalRankingExpressions
+ */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public List<String> getInformationalRankingExpressions() {
+ return mInformationalRankingExpressions;
}
/**
* Returns whether the NUMERIC_SEARCH feature is enabled.
*/
public boolean isNumericSearchEnabled() {
- return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH);
+ return mEnabledFeatures.contains(FeatureConstants.NUMERIC_SEARCH);
}
/**
* Returns whether the VERBATIM_SEARCH feature is enabled.
*/
public boolean isVerbatimSearchEnabled() {
- return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH);
+ return mEnabledFeatures.contains(FeatureConstants.VERBATIM_SEARCH);
}
/**
* Returns whether the LIST_FILTER_QUERY_LANGUAGE feature is enabled.
*/
public boolean isListFilterQueryLanguageEnabled() {
- return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
+ }
+
+ /**
+ * Returns whether the LIST_FILTER_HAS_PROPERTY_FUNCTION feature is enabled.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)
+ public boolean isListFilterHasPropertyFunctionEnabled() {
+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION);
+ }
+
+ /**
+ * Returns whether the embedding search feature is enabled.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public boolean isEmbeddingSearchEnabled() {
+ return mEnabledFeatures.contains(FeatureConstants.EMBEDDING_SEARCH);
+ }
+
+ /**
+ * Returns whether the LIST_FILTER_TOKENIZE_FUNCTION feature is enabled.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public boolean isListFilterTokenizeFunctionEnabled() {
+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
}
/**
@@ -539,21 +757,31 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public List<String> getEnabledFeatures() {
- return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD);
+ return mEnabledFeatures;
+ }
+
+ @Override
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SearchSpecCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link SearchSpec objects}. */
public static final class Builder {
- private ArrayList<String> mSchemas = new ArrayList<>();
- private ArrayList<String> mNamespaces = new ArrayList<>();
+ private List<String> mSchemas = new ArrayList<>();
+ private List<String> mNamespaces = new ArrayList<>();
private Bundle mTypePropertyFilters = new Bundle();
- private ArrayList<String> mPackageNames = new ArrayList<>();
+ private List<String> mPackageNames = new ArrayList<>();
private ArraySet<String> mEnabledFeatures = new ArraySet<>();
private Bundle mProjectionTypePropertyMasks = new Bundle();
private Bundle mTypePropertyWeights = new Bundle();
+ private List<EmbeddingVector> mSearchEmbeddings = new ArrayList<>();
private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
@TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;
+ @EmbeddingSearchMetricType
+ private int mDefaultEmbeddingSearchMetricType = EMBEDDING_SEARCH_METRIC_TYPE_COSINE;
private int mSnippetCount = 0;
private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
private int mMaxSnippetSize = 0;
@@ -561,10 +789,53 @@
@Order private int mOrder = ORDER_DESCENDING;
@GroupingType private int mGroupingTypeFlags = 0;
private int mGroupingLimit = 0;
- private JoinSpec mJoinSpec;
+ @Nullable private JoinSpec mJoinSpec;
private String mAdvancedRankingExpression = "";
+ private List<String> mInformationalRankingExpressions = new ArrayList<>();
+ @Nullable private String mSearchSourceLogTag;
private boolean mBuilt = false;
+ /** Constructs a new builder for {@link SearchSpec} objects. */
+ public Builder() {
+ }
+
+ /** @exportToFramework:hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull SearchSpec searchSpec) {
+ Objects.requireNonNull(searchSpec);
+ mSchemas = new ArrayList<>(searchSpec.getFilterSchemas());
+ mNamespaces = new ArrayList<>(searchSpec.getFilterNamespaces());
+ for (Map.Entry<String, List<String>> entry :
+ searchSpec.getFilterProperties().entrySet()) {
+ addFilterProperties(entry.getKey(), entry.getValue());
+ }
+ mPackageNames = new ArrayList<>(searchSpec.getFilterPackageNames());
+ mEnabledFeatures = new ArraySet<>(searchSpec.getEnabledFeatures());
+ for (Map.Entry<String, List<String>> entry : searchSpec.getProjections().entrySet()) {
+ addProjection(entry.getKey(), entry.getValue());
+ }
+ for (Map.Entry<String, Map<String, Double>> entry :
+ searchSpec.getPropertyWeights().entrySet()) {
+ setPropertyWeights(entry.getKey(), entry.getValue());
+ }
+ mSearchEmbeddings = new ArrayList<>(searchSpec.getSearchEmbeddings());
+ mResultCountPerPage = searchSpec.getResultCountPerPage();
+ mTermMatchType = searchSpec.getTermMatch();
+ mDefaultEmbeddingSearchMetricType = searchSpec.getDefaultEmbeddingSearchMetricType();
+ mSnippetCount = searchSpec.getSnippetCount();
+ mSnippetCountPerProperty = searchSpec.getSnippetCountPerProperty();
+ mMaxSnippetSize = searchSpec.getMaxSnippetSize();
+ mRankingStrategy = searchSpec.getRankingStrategy();
+ mOrder = searchSpec.getOrder();
+ mGroupingTypeFlags = searchSpec.getResultGroupingTypeFlags();
+ mGroupingLimit = searchSpec.getResultGroupingLimit();
+ mJoinSpec = searchSpec.getJoinSpec();
+ mAdvancedRankingExpression = searchSpec.getAdvancedRankingExpression();
+ mInformationalRankingExpressions = new ArrayList<>(
+ searchSpec.getInformationalRankingExpressions());
+ mSearchSourceLogTag = searchSpec.getSearchSourceLogTag();
+ }
+
/**
* Sets how the query terms should match {@code TermMatchCode} in the index.
*
@@ -626,12 +897,13 @@
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addFilterDocumentClasses(
- @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+ @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+ throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
List<String> schemas = new ArrayList<>(documentClasses.size());
DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
- for (Class<?> documentClass : documentClasses) {
+ for (java.lang.Class<?> documentClass : documentClasses) {
DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
schemas.add(factory.getSchemaName());
}
@@ -655,7 +927,7 @@
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+ public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
@@ -685,17 +957,13 @@
* @param schema the {@link AppSearchSchema} that contains the target properties
* @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
* sequence of property names.
- *
- * @exportToFramework:hide
*/
- // TODO(b/296088047) unhide from framework when type property filters are made public.
+ @CanIgnoreReturnValue
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
- // @exportToFramework:endStrip()
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Builder addFilterProperties(@NonNull String schema,
@NonNull Collection<String> propertyPaths) {
Preconditions.checkNotNull(schema);
@@ -720,17 +988,14 @@
*
* @param schema the {@link AppSearchSchema} that contains the target properties
* @param propertyPaths The {@link PropertyPath} to search search over
- *
- * @exportToFramework:hide
*/
- // TODO(b/296088047) unhide from framework when type property filters are made public.
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- // @exportToFramework:startStrip()
+ // Getter method is getFilterProperties
+ @SuppressLint("MissingGetterMatchingBuilder")
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
- // @exportToFramework:endStrip()
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Builder addFilterPropertyPaths(@NonNull String schema,
@NonNull Collection<PropertyPath> propertyPaths) {
Preconditions.checkNotNull(schema);
@@ -744,6 +1009,7 @@
// @exportToFramework:startStrip()
+
/**
* Adds property paths for the specified type to the property filter of
* {@link SearchSpec} Entry. Only returns documents that have matches under the specified
@@ -758,11 +1024,10 @@
*
*/
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
- public Builder addFilterProperties(@NonNull Class<?> documentClass,
+ public Builder addFilterProperties(@NonNull java.lang.Class<?> documentClass,
@NonNull Collection<String> propertyPaths) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
Preconditions.checkNotNull(propertyPaths);
@@ -787,11 +1052,12 @@
*
*/
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // Getter method is getFilterProperties
+ @SuppressLint("MissingGetterMatchingBuilder")
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
- public Builder addFilterPropertyPaths(@NonNull Class<?> documentClass,
+ public Builder addFilterPropertyPaths(@NonNull java.lang.Class<?> documentClass,
@NonNull Collection<PropertyPath> propertyPaths) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
Preconditions.checkNotNull(propertyPaths);
@@ -966,6 +1232,19 @@
* current document being scored. Property weights come from what's specified in
* {@link SearchSpec}. After normalizing, each provided weight will be divided by the
* maximum weight, so that each of them will be <= 1.
+ * <li>this.matchedSemanticScores(getSearchSpecEmbedding({embedding_index}), {metric})
+ * <p>Returns a list of the matched similarity scores from "semanticSearch" in the query
+ * expression (see also {@link AppSearchSession#search}) based on embedding_index and
+ * metric. If metric is omitted, it defaults to the metric specified in
+ * {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. If no
+ * "semanticSearch" is called for embedding_index and metric in the query, this
+ * function will return an empty list. If multiple "semanticSearch"s are called for
+ * the same embedding_index and metric, this function will return a list of their
+ * merged scores.
+ * <p>Example: `this.matchedSemanticScores(getSearchSpecEmbedding(0), "COSINE")` will
+ * return a list of matched scores within the range of [0.5, 1], if
+ * `semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")` is called in the
+ * query expression.
* </ul>
*
* <p>Some errors may occur when using advanced ranking.
@@ -1011,11 +1290,9 @@
*/
@CanIgnoreReturnValue
@NonNull
- // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)
- // @exportToFramework:endStrip()
public Builder setRankingStrategy(@NonNull String advancedRankingExpression) {
Preconditions.checkStringNotEmpty(advancedRankingExpression);
resetIfBuilt();
@@ -1025,6 +1302,88 @@
}
/**
+ * Adds informational ranking expressions to be evaluated for each document in the search
+ * result. The values of these expressions will be returned to the caller via
+ * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for
+ * the caller to retrieve additional information about the result and have no effect on
+ * ranking.
+ *
+ * <p>The syntax is exactly the same as specified in
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)
+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public Builder addInformationalRankingExpressions(
+ @NonNull String... informationalRankingExpressions) {
+ Preconditions.checkNotNull(informationalRankingExpressions);
+ resetIfBuilt();
+ return addInformationalRankingExpressions(
+ Arrays.asList(informationalRankingExpressions));
+ }
+
+ /**
+ * Adds informational ranking expressions to be evaluated for each document in the search
+ * result. The values of these expressions will be returned to the caller via
+ * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for
+ * the caller to retrieve additional information about the result and have no effect on
+ * ranking.
+ *
+ * <p>The syntax is exactly the same as specified in
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)
+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+ public Builder addInformationalRankingExpressions(
+ @NonNull Collection<String> informationalRankingExpressions) {
+ Preconditions.checkNotNull(informationalRankingExpressions);
+ resetIfBuilt();
+ mInformationalRankingExpressions.addAll(informationalRankingExpressions);
+ return this;
+ }
+
+ /**
+ * Sets an optional log tag to indicate the source of this search.
+ *
+ * <p>Some AppSearch implementations may log a hash of this tag using statsd. This tag
+ * may be used for tracing performance issues and crashes to a component of an app.
+ *
+ * <p>Call this method and give a unique value if you want to distinguish this search
+ * scenario with other search scenarios during performance analysis.
+ *
+ * <p>Under no circumstances will AppSearch log the raw String value using statsd, but it
+ * will be provided as-is to custom {@code AppSearchLogger} implementations you have
+ * registered in your app.
+ *
+ * @param searchSourceLogTag A String to indicate the source caller of this search. It is
+ * used to label the search statsd for performance analysis. It
+ * is not the tag we are using in {@link android.util.Log}. The
+ * length of the teg should between 1 and 100.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+ public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) {
+ Preconditions.checkStringNotEmpty(searchSourceLogTag);
+ Preconditions.checkArgument(searchSourceLogTag.length() <= 100,
+ "The maximum supported tag length is 100. This tag is too long: "
+ + searchSourceLogTag.length());
+ resetIfBuilt();
+ mSearchSourceLogTag = searchSourceLogTag;
+ return this;
+ }
+
+ /**
* Sets the order of returned search results, the default is
* {@link #ORDER_DESCENDING}, meaning that results with higher scores come first.
*
@@ -1143,9 +1502,8 @@
* results of that type will be retrieved.
*
* <p>If property path is added for the
- * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
- * apply to all results, excepting any types that have their own, specific property paths
- * set.
+ * {@link SearchSpec#SCHEMA_TYPE_WILDCARD}, then those property paths will apply to all
+ * results, excepting any types that have their own, specific property paths set.
*
* <p>Suppose the following document is in the index.
* <pre>{@code
@@ -1226,7 +1584,8 @@
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections
@NonNull
public SearchSpec.Builder addProjectionsForDocumentClass(
- @NonNull Class<?> documentClass, @NonNull Collection<String> propertyPaths)
+ @NonNull java.lang.Class<?> documentClass,
+ @NonNull Collection<String> propertyPaths)
throws AppSearchException {
Preconditions.checkNotNull(documentClass);
resetIfBuilt();
@@ -1247,7 +1606,8 @@
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections
@NonNull
public SearchSpec.Builder addProjectionPathsForDocumentClass(
- @NonNull Class<?> documentClass, @NonNull Collection<PropertyPath> propertyPaths)
+ @NonNull java.lang.Class<?> documentClass,
+ @NonNull Collection<PropertyPath> propertyPaths)
throws AppSearchException {
Preconditions.checkNotNull(documentClass);
resetIfBuilt();
@@ -1319,12 +1679,10 @@
* weight to set for that property.
* @throws IllegalArgumentException if a weight is equal to or less than 0.0.
*/
- // @exportToFramework:startStrip()
@CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
- // @exportToFramework:endStrip()
@NonNull
public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType,
@NonNull Map<String, Double> propertyPathWeights) {
@@ -1354,12 +1712,10 @@
*
* @param joinSpec a specification on how to perform the Join operation.
*/
- // @exportToFramework:startStrip()
@CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
- // @exportToFramework:endStrip()
@NonNull
public Builder setJoinSpec(@NonNull JoinSpec joinSpec) {
resetIfBuilt();
@@ -1397,12 +1753,10 @@
* weight to set for that property.
* @throws IllegalArgumentException if a weight is equal to or less than 0.0.
*/
- // @exportToFramework:startStrip()
@CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
- // @exportToFramework:endStrip()
@NonNull
public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType,
@NonNull Map<PropertyPath, Double> propertyPathWeights) {
@@ -1460,7 +1814,7 @@
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
@NonNull
public SearchSpec.Builder setPropertyWeightsForDocumentClass(
- @NonNull Class<?> documentClass,
+ @NonNull java.lang.Class<?> documentClass,
@NonNull Map<String, Double> propertyPathWeights) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
@@ -1508,7 +1862,7 @@
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
@NonNull
public SearchSpec.Builder setPropertyWeightPathsForDocumentClass(
- @NonNull Class<?> documentClass,
+ @NonNull java.lang.Class<?> documentClass,
@NonNull Map<PropertyPath, Double> propertyPathWeights) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
@@ -1518,6 +1872,72 @@
// @exportToFramework:endStrip()
/**
+ * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the
+ * query expression and the ranking expression for embedding search.
+ *
+ * @see AppSearchSession#search
+ * @see SearchSpec.Builder#setRankingStrategy(String)
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public Builder addSearchEmbeddings(@NonNull EmbeddingVector... searchEmbeddings) {
+ Preconditions.checkNotNull(searchEmbeddings);
+ resetIfBuilt();
+ return addSearchEmbeddings(Arrays.asList(searchEmbeddings));
+ }
+
+ /**
+ * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the
+ * query expression and the ranking expression for embedding search.
+ *
+ * @see AppSearchSession#search
+ * @see SearchSpec.Builder#setRankingStrategy(String)
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public Builder addSearchEmbeddings(
+ @NonNull Collection<EmbeddingVector> searchEmbeddings) {
+ Preconditions.checkNotNull(searchEmbeddings);
+ resetIfBuilt();
+ mSearchEmbeddings.addAll(searchEmbeddings);
+ return this;
+ }
+
+ /**
+ * Sets the default embedding metric type used for embedding search
+ * (see {@link AppSearchSession#search}) and ranking
+ * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).
+ *
+ * <p>If this method is not called, the default embedding search metric type is
+ * {@link SearchSpec#EMBEDDING_SEARCH_METRIC_TYPE_COSINE}. Metrics specified within
+ * "semanticSearch" or "matchedSemanticScores" functions in search/ranking expressions
+ * will override this default.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public Builder setDefaultEmbeddingSearchMetricType(
+ @EmbeddingSearchMetricType int defaultEmbeddingSearchMetricType) {
+ Preconditions.checkArgumentInRange(defaultEmbeddingSearchMetricType,
+ EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+ EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN, "Embedding search metric type");
+ resetIfBuilt();
+ mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+ return this;
+ }
+
+ /**
* Sets the NUMERIC_SEARCH feature as enabled/disabled according to the enabled parameter.
*
* @param enabled Enables the feature if true, otherwise disables it.
@@ -1526,11 +1946,10 @@
* {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric
* querying features.
*/
- // @exportToFramework:startStrip()
+ @CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.NUMERIC_SEARCH)
- // @exportToFramework:endStrip()
@NonNull
public Builder setNumericSearchEnabled(boolean enabled) {
modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);
@@ -1550,11 +1969,10 @@
* <p>For example, The verbatim string operator '"foo/bar" OR baz' will ensure that
* 'foo/bar' is treated as a single 'verbatim' token.
*/
- // @exportToFramework:startStrip()
+ @CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.VERBATIM_SEARCH)
- // @exportToFramework:endStrip()
@NonNull
public Builder setVerbatimSearchEnabled(boolean enabled) {
modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);
@@ -1578,7 +1996,7 @@
* <p>The newly added custom functions covered by this feature are:
* <ul>
* <li>createList(String...)</li>
- * <li>termSearch(String, List<String>)</li>
+ * <li>termSearch(String, {@code List<String>})</li>
* </ul>
*
* <p>createList takes a variable number of strings and returns a list of strings.
@@ -1590,11 +2008,10 @@
* for example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)"
* could be rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))"
*/
- // @exportToFramework:startStrip()
+ @CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.LIST_FILTER_QUERY_LANGUAGE)
- // @exportToFramework:endStrip()
@NonNull
public Builder setListFilterQueryLanguageEnabled(boolean enabled) {
modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);
@@ -1602,6 +2019,65 @@
}
/**
+ * Sets the LIST_FILTER_HAS_PROPERTY_FUNCTION feature as enabled/disabled according to
+ * the enabled parameter.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it
+ *
+ * <p>If disabled, disallows the use of the "hasProperty" function. See
+ * {@link AppSearchSession#search} for more details about the function.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)
+ public Builder setListFilterHasPropertyFunctionEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION, enabled);
+ return this;
+ }
+
+ /**
+ * Sets the embedding search feature as enabled/disabled according to the enabled parameter.
+ *
+ * <p>If disabled, disallows the use of the "semanticSearch" function. See
+ * {@link AppSearchSession#search} for more details about the function.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+ public Builder setEmbeddingSearchEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.EMBEDDING_SEARCH, enabled);
+ return this;
+ }
+
+ /**
+ * Sets the LIST_FILTER_TOKENIZE_FUNCTION feature as enabled/disabled according to
+ * the enabled parameter.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it
+ *
+ * <p>If disabled, disallows the use of the "tokenize" function. See
+ * {@link AppSearchSession#search} for more details about the function.
+ */
+ @CanIgnoreReturnValue
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.LIST_FILTER_TOKENIZE_FUNCTION)
+ @NonNull
+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+ public Builder setListFilterTokenizeFunctionEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION, enabled);
+ return this;
+ }
+
+ /**
* Constructs a new {@link SearchSpec} from the contents of this builder.
*
* @throws IllegalArgumentException if property weights are provided with a
@@ -1617,7 +2093,6 @@
*/
@NonNull
public SearchSpec build() {
- Bundle bundle = new Bundle();
if (mJoinSpec != null) {
if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
&& mJoinSpec.getAggregationScoringStrategy()
@@ -1626,59 +2101,26 @@
+ "the nested JoinSpec, but ranking strategy is not "
+ "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE");
}
- bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle());
} else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) {
throw new IllegalStateException("Attempting to rank based on joined documents, but "
+ "no JoinSpec provided");
}
if (!mTypePropertyWeights.isEmpty()
- && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy
- && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {
+ && mRankingStrategy != RANKING_STRATEGY_RELEVANCE_SCORE
+ && mRankingStrategy != RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION) {
throw new IllegalArgumentException("Property weights are only compatible with the "
+ "RANKING_STRATEGY_RELEVANCE_SCORE and "
+ "RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies.");
}
- // If the schema filter isn't empty, and there is a schema with a projection but not
- // in the filter, that is a SearchSpec user error.
- if (!mSchemas.isEmpty()) {
- for (String schema : mProjectionTypePropertyMasks.keySet()) {
- if (!mSchemas.contains(schema)) {
- throw new IllegalArgumentException("Projection requested for schema not "
- + "in schemas filters: " + schema);
- }
- }
- }
-
- Set<String> schemaFilter = new ArraySet<>(mSchemas);
- if (!mSchemas.isEmpty()) {
- for (String schema : mTypePropertyFilters.keySet()) {
- if (!schemaFilter.contains(schema)) {
- throw new IllegalStateException(
- "The schema: " + schema + " exists in the property filter but "
- + "doesn't exist in the schema filter.");
- }
- }
- }
- bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
- bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
- bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
- bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
- bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures));
- bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
- bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
- bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
- bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
- bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
- bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
- bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
- bundle.putInt(ORDER_FIELD, mOrder);
- bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
- bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
- bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);
- bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression);
mBuilt = true;
- return new SearchSpec(bundle);
+ return new SearchSpec(mTermMatchType, mSchemas, mNamespaces,
+ mTypePropertyFilters, mPackageNames, mResultCountPerPage,
+ mRankingStrategy, mOrder, mSnippetCount, mSnippetCountPerProperty,
+ mMaxSnippetSize, mProjectionTypePropertyMasks, mGroupingTypeFlags,
+ mGroupingLimit, mTypePropertyWeights, mJoinSpec, mAdvancedRankingExpression,
+ new ArrayList<>(mEnabledFeatures), mSearchSourceLogTag, mSearchEmbeddings,
+ mDefaultEmbeddingSearchMetricType, mInformationalRankingExpressions);
}
private void resetIfBuilt() {
@@ -1689,6 +2131,9 @@
mPackageNames = new ArrayList<>(mPackageNames);
mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);
+ mSearchEmbeddings = new ArrayList<>(mSearchEmbeddings);
+ mInformationalRankingExpressions = new ArrayList<>(
+ mInformationalRankingExpressions);
mBuilt = false;
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
index 63fd696..c5171fd1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
@@ -16,38 +16,40 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionResultCreator;
import androidx.core.util.Preconditions;
/**
* The result class of the {@link AppSearchSession#searchSuggestionAsync}.
*/
-public final class SearchSuggestionResult {
[email protected](creator = "SearchSuggestionResultCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSuggestionResult extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<SearchSuggestionResult> CREATOR =
+ new SearchSuggestionResultCreator();
- private static final String SUGGESTED_RESULT_FIELD = "suggestedResult";
- private final Bundle mBundle;
+ @Field(id = 1, getter = "getSuggestedResult")
+ private final String mSuggestedResult;
@Nullable
private Integer mHashCode;
- SearchSuggestionResult(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
-
- /**
- * Returns the {@link Bundle} populated by this builder.
- *
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
+ @Constructor
+ SearchSuggestionResult(@Param(id = 1) String suggestedResult) {
+ mSuggestedResult = Preconditions.checkNotNull(suggestedResult);
}
/**
@@ -60,7 +62,7 @@
*/
@NonNull
public String getSuggestedResult() {
- return Preconditions.checkNotNull(mBundle.getString(SUGGESTED_RESULT_FIELD));
+ return mSuggestedResult;
}
@Override
@@ -72,13 +74,13 @@
return false;
}
SearchSuggestionResult otherResult = (SearchSuggestionResult) other;
- return BundleUtil.deepEquals(this.mBundle, otherResult.mBundle);
+ return mSuggestedResult.equals(otherResult.mSuggestedResult);
}
@Override
public int hashCode() {
if (mHashCode == null) {
- mHashCode = BundleUtil.deepHashCode(mBundle);
+ mHashCode = mSuggestedResult.hashCode();
}
return mHashCode;
}
@@ -102,12 +104,17 @@
return this;
}
- /** Build a {@link SearchSuggestionResult} object*/
+ /** Build a {@link SearchSuggestionResult} object */
@NonNull
public SearchSuggestionResult build() {
- Bundle bundle = new Bundle();
- bundle.putString(SUGGESTED_RESULT_FIELD, mSuggestedResult);
- return new SearchSuggestionResult(bundle);
+ return new SearchSuggestionResult(mSuggestedResult);
}
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SearchSuggestionResultCreator.writeToParcel(this, dest, flags);
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
index 4b87f9c..f2db304 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
@@ -18,14 +18,22 @@
import android.annotation.SuppressLint;
import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionSpecCreator;
import androidx.appsearch.util.BundleUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
@@ -47,24 +55,48 @@
*
* @see AppSearchSession#searchSuggestionAsync
*/
-public final class SearchSuggestionSpec {
- static final String NAMESPACE_FIELD = "namespace";
- static final String SCHEMA_FIELD = "schema";
- static final String PROPERTY_FIELD = "property";
- static final String DOCUMENT_IDS_FIELD = "documentIds";
- static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";
- static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
- private final Bundle mBundle;
[email protected](creator = "SearchSuggestionSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSuggestionSpec extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<SearchSuggestionSpec> CREATOR =
+ new SearchSuggestionSpecCreator();
+ @Field(id = 1, getter = "getFilterNamespaces")
+ private final List<String> mFilterNamespaces;
+ @Field(id = 2, getter = "getFilterSchemas")
+ private final List<String> mFilterSchemas;
+ // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
+ // schema type and value is a list of target property paths in that schema to search over.
+ @Field(id = 3)
+ final Bundle mFilterProperties;
+ // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
+ // namespace and value is a list of target document ids in that namespace to search over.
+ @Field(id = 4)
+ final Bundle mFilterDocumentIds;
+ @Field(id = 5, getter = "getRankingStrategy")
+ private final int mRankingStrategy;
+ @Field(id = 6, getter = "getMaximumResultCount")
private final int mMaximumResultCount;
/** @exportToFramework:hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public SearchSuggestionSpec(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
- mMaximumResultCount = bundle.getInt(MAXIMUM_RESULT_COUNT_FIELD);
- Preconditions.checkArgument(mMaximumResultCount >= 1,
+ @Constructor
+ public SearchSuggestionSpec(
+ @Param(id = 1) @NonNull List<String> filterNamespaces,
+ @Param(id = 2) @NonNull List<String> filterSchemas,
+ @Param(id = 3) @NonNull Bundle filterProperties,
+ @Param(id = 4) @NonNull Bundle filterDocumentIds,
+ @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy,
+ @Param(id = 6) int maximumResultCount) {
+ Preconditions.checkArgument(maximumResultCount >= 1,
"MaximumResultCount must be positive.");
+ mFilterNamespaces = Preconditions.checkNotNull(filterNamespaces);
+ mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
+ mFilterProperties = Preconditions.checkNotNull(filterProperties);
+ mFilterDocumentIds = Preconditions.checkNotNull(filterDocumentIds);
+ mRankingStrategy = rankingStrategy;
+ mMaximumResultCount = maximumResultCount;
}
/**
@@ -111,17 +143,6 @@
public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2;
/**
- * Returns the {@link Bundle} populated by this builder.
- *
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
- }
-
- /**
* Returns the maximum number of wanted suggestion that will be returned in the result object.
*/
public int getMaximumResultCount() {
@@ -135,17 +156,16 @@
*/
@NonNull
public List<String> getFilterNamespaces() {
- List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
- if (namespaces == null) {
+ if (mFilterNamespaces == null) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(namespaces);
+ return Collections.unmodifiableList(mFilterNamespaces);
}
/** Returns the ranking strategy. */
@SuggestionRankingStrategy
public int getRankingStrategy() {
- return mBundle.getInt(RANKING_STRATEGY_FIELD);
+ return mRankingStrategy;
}
/**
@@ -155,11 +175,10 @@
*/
@NonNull
public List<String> getFilterSchemas() {
- List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_FIELD);
- if (schemaTypes == null) {
+ if (mFilterSchemas == null) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(schemaTypes);
+ return Collections.unmodifiableList(mFilterSchemas);
}
/**
@@ -173,20 +192,15 @@
*
* <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.
- *
- * @exportToFramework:hide
*/
- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Map<String, List<String>> getFilterProperties() {
- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
- mBundle.getBundle(PROPERTY_FIELD));
- Set<String> schemas = typePropertyPathsBundle.keySet();
+ Set<String> schemas = mFilterProperties.keySet();
Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
for (String schema : schemas) {
typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
- typePropertyPathsBundle.getStringArrayList(schema)));
+ mFilterProperties.getStringArrayList(schema)));
}
return typePropertyPathsMap;
}
@@ -205,13 +219,11 @@
*/
@NonNull
public Map<String, List<String>> getFilterDocumentIds() {
- Bundle documentIdsBundle = Preconditions.checkNotNull(
- mBundle.getBundle(DOCUMENT_IDS_FIELD));
- Set<String> namespaces = documentIdsBundle.keySet();
+ Set<String> namespaces = mFilterDocumentIds.keySet();
Map<String, List<String>> documentIdsMap = new ArrayMap<>(namespaces.size());
for (String namespace : namespaces) {
documentIdsMap.put(namespace, Preconditions.checkNotNull(
- documentIdsBundle.getStringArrayList(namespace)));
+ mFilterDocumentIds.getStringArrayList(namespace)));
}
return documentIdsMap;
}
@@ -328,7 +340,7 @@
@SuppressLint("MissingGetterMatchingBuilder")
@CanIgnoreReturnValue
@NonNull
- public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+ public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
@@ -353,12 +365,13 @@
@CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(
- @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+ @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+ throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
List<String> schemas = new ArrayList<>(documentClasses.size());
DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
- for (Class<?> documentClass : documentClasses) {
+ for (java.lang.Class<?> documentClass : documentClasses) {
DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
schemas.add(factory.getSchemaName());
}
@@ -385,11 +398,13 @@
* @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
* sequence of property names indicating which property in the
* document these snippets correspond to.
- * @exportToFramework:hide
*/
- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+ @CanIgnoreReturnValue
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Builder addFilterProperties(@NonNull String schema,
@NonNull Collection<String> propertyPaths) {
Preconditions.checkNotNull(schema);
@@ -418,12 +433,15 @@
*
* @param schema the {@link AppSearchSchema} that contains the target properties
* @param propertyPaths The {@link PropertyPath} to search suggestion over
- *
- * @exportToFramework:hide
*/
- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+ @CanIgnoreReturnValue
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // Getter method is getFilterProperties
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
public Builder addFilterPropertyPaths(@NonNull String schema,
@NonNull Collection<PropertyPath> propertyPaths) {
Preconditions.checkNotNull(schema);
@@ -453,12 +471,12 @@
* @param propertyPaths The String version of {@link PropertyPath}. A
* {@code dot-delimited sequence of property names indicating which property in the
* document these snippets correspond to.
- * @exportToFramework:hide
*/
- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Builder addFilterProperties(@NonNull Class<?> documentClass,
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+ public Builder addFilterProperties(@NonNull java.lang.Class<?> documentClass,
@NonNull Collection<String> propertyPaths) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
Preconditions.checkNotNull(propertyPaths);
@@ -484,12 +502,14 @@
*
* @param documentClass class annotated with {@link Document}.
* @param propertyPaths The {@link PropertyPath} to search suggestion over
- * @exportToFramework:hide
*/
- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Builder addFilterPropertyPaths(@NonNull Class<?> documentClass,
+ // Getter method is getFilterProperties
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+ public Builder addFilterPropertyPaths(@NonNull java.lang.Class<?> documentClass,
@NonNull Collection<PropertyPath> propertyPaths) throws AppSearchException {
Preconditions.checkNotNull(documentClass);
Preconditions.checkNotNull(propertyPaths);
@@ -540,7 +560,6 @@
/** Constructs a new {@link SearchSpec} from the contents of this builder. */
@NonNull
public SearchSuggestionSpec build() {
- Bundle bundle = new Bundle();
if (!mSchemas.isEmpty()) {
Set<String> schemaFilter = new ArraySet<>(mSchemas);
for (String schema : mTypePropertyFilters.keySet()) {
@@ -561,14 +580,14 @@
}
}
}
- bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
- bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
- bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
- bundle.putBundle(DOCUMENT_IDS_FIELD, mDocumentIds);
- bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount);
- bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
mBuilt = true;
- return new SearchSuggestionSpec(bundle);
+ return new SearchSuggestionSpec(
+ mNamespaces,
+ mSchemas,
+ mTypePropertyFilters,
+ mDocumentIds,
+ mRankingStrategy,
+ mTotalResultCount);
}
private void resetIfBuilt() {
@@ -581,4 +600,11 @@
}
}
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SearchSuggestionSpecCreator.writeToParcel(this, dest, flags);
+ }
}
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 6cc0e85..3cabaab 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -21,10 +21,13 @@
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -100,13 +103,13 @@
READ_EXTERNAL_STORAGE,
READ_HOME_APP_SEARCH_DATA,
READ_ASSISTANT_APP_SEARCH_DATA,
+ ENTERPRISE_ACCESS,
+ MANAGED_PROFILE_CONTACTS_ACCESS,
})
@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 {}
@@ -114,72 +117,85 @@
* 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;
+ /**
+ * A schema must have this permission set through {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an
+ * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will
+ * not count as having this permission.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int ENTERPRISE_ACCESS = 7;
+
+ /**
+ * A schema with this permission set through {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller
+ * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to
+ * be visible. This permission indicates that the protected schema may expose managed profile
+ * data for contacts search.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
+
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, PackageIdentifier> mPubliclyVisibleSchemas;
+ private final Map<String, Set<SchemaVisibilityConfig>> mSchemasVisibleToConfigs;
private final Map<String, Migrator> mMigrators;
private final boolean mForceOverride;
private final int mVersion;
@@ -188,6 +204,8 @@
@NonNull Set<String> schemasNotDisplayedBySystem,
@NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
@NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
+ @NonNull Map<String, PackageIdentifier> publiclyVisibleSchemas,
+ @NonNull Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs,
@NonNull Map<String, Migrator> migrators,
boolean forceOverride,
int version) {
@@ -195,6 +213,8 @@
mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);
+ mPubliclyVisibleSchemas = Preconditions.checkNotNull(publiclyVisibleSchemas);
+ mSchemasVisibleToConfigs = Preconditions.checkNotNull(schemasVisibleToConfigs);
mMigrators = Preconditions.checkNotNull(migrators);
mForceOverride = forceOverride;
mVersion = version;
@@ -258,12 +278,41 @@
* {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
* {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
*/
+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
@NonNull
public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
return deepCopy(mSchemasVisibleToPermissions);
}
/**
+ * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying
+ * the package the schemas are from.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ @NonNull
+ public Map<String, PackageIdentifier> getPubliclyVisibleSchemas() {
+ return Collections.unmodifiableMap(mPubliclyVisibleSchemas);
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
+ * access to that schema type.
+ *
+ * <p>It’s inefficient to call this method repeatedly.
+ * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @NonNull
+ public Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs() {
+ Map<String, Set<SchemaVisibilityConfig>> copy = new ArrayMap<>();
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ mSchemasVisibleToConfigs.entrySet()) {
+ copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ return copy;
+ }
+
+ /**
* Returns the map of {@link Migrator}, the key will be the schema type of the
* {@link Migrator} associated with.
*/
@@ -307,6 +356,9 @@
private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
new ArrayMap<>();
private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
+ private ArrayMap<String, PackageIdentifier> mPubliclyVisibleSchemas = new ArrayMap<>();
+ private ArrayMap<String, Set<SchemaVisibilityConfig>> mSchemaVisibleToConfigs =
+ new ArrayMap<>();
private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
private boolean mForceOverride = false;
private int mVersion = DEFAULT_VERSION;
@@ -451,10 +503,16 @@
* <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},
+ * <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}.
*
+ * <p> The relationship between permissions added in this method and package visibility
+ * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access
+ * the schema if they match ANY requirements. If you want to set "AND" requirements like
+ * a caller must hold required permissions AND it is a specified package, please use
+ * {@link #addSchemaTypeVisibleToConfig}.
+ *
* @see android.Manifest.permission#READ_SMS
* @see android.Manifest.permission#READ_CALENDAR
* @see android.Manifest.permission#READ_CONTACTS
@@ -467,14 +525,13 @@
* schema.
* @throws IllegalArgumentException – if input unsupported permission.
*/
+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
// Merged list available from getRequiredPermissionsForSchemaTypeVisibility
@CanIgnoreReturnValue
@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) {
@@ -482,7 +539,7 @@
Preconditions.checkNotNull(permissions);
for (int permission : permissions) {
Preconditions.checkArgumentInRange(permission, READ_SMS,
- READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+ MANAGED_PROFILE_CONTACTS_ACCESS, "permission");
}
resetIfBuilt();
Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
@@ -495,12 +552,10 @@
}
/** Clears all required permissions combinations for the given schema type. */
- // @exportToFramework:startStrip()
@CanIgnoreReturnValue
@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);
@@ -525,6 +580,12 @@
*
* <p>By default, data sharing between applications is disabled.
*
+ * <p> The relationship between permissions added in this method and package visibility
+ * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access
+ * the schema if they match ANY requirements. If you want to set "AND" requirements like
+ * a caller must hold required permissions AND it is a specified package, please use
+ * {@link #addSchemaTypeVisibleToConfig}.
+ *
* @param schemaType The schema type to set visibility on.
* @param visible Whether the {@code schemaType} will be visible or not.
* @param packageIdentifier Represents the package that will be granted visibility.
@@ -564,6 +625,132 @@
}
/**
+ * Specify that the schema should be publicly available, to packages which already have
+ * visibility to {@code packageIdentifier}. This visibility is determined by the result of
+ * {@link android.content.pm.PackageManager#canPackageQuery}.
+ *
+ * <p> It is possible for the packageIdentifier parameter to be different from the
+ * package performing the indexing. This might happen in the case of an on-device indexer
+ * processing information about various packages. The visibility will be the same
+ * regardless of which package indexes the document, as the visibility is based on the
+ * packageIdentifier parameter.
+ *
+ * <p> If this is called repeatedly with the same schema, the {@link PackageIdentifier} in
+ * the last call will be used as the "from" package for that schema.
+ *
+ * <p> Calling this with packageIdentifier set to null is valid, and will remove public
+ * visibility for the schema.
+ *
+ * @param schema the schema to make publicly accessible.
+ * @param packageIdentifier if an app can see this package via
+ * PackageManager#canPackageQuery, it will be able to see the
+ * documents of type {@code schema}.
+ */
+ // Merged list available from getPubliclyVisibleSchemas
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ @NonNull
+ public Builder setPubliclyVisibleSchema(@NonNull String schema,
+ @Nullable PackageIdentifier packageIdentifier) {
+ Preconditions.checkNotNull(schema);
+ resetIfBuilt();
+
+ // If the package identifier is null or empty we clear public visibility
+ if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) {
+ mPubliclyVisibleSchemas.remove(schema);
+ return this;
+ }
+
+ mPubliclyVisibleSchemas.put(schema, packageIdentifier);
+ return this;
+ }
+
+// @exportToFramework:startStrip()
+ /**
+ * Specify that the schema should be publicly available, to packages which already have
+ * visibility to {@code packageIdentifier}.
+ *
+ * @param documentClass the document to make publicly accessible.
+ * @param packageIdentifier if an app can see this package via
+ * PackageManager#canPackageQuery, it will be able to see the
+ * documents of type {@code documentClass}.
+ * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema
+ */
+ // Merged list available from getPubliclyVisibleSchemas
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+ @NonNull
+ public Builder setPubliclyVisibleDocumentClass(@NonNull Class<?> documentClass,
+ @Nullable PackageIdentifier packageIdentifier) throws AppSearchException {
+ Preconditions.checkNotNull(documentClass);
+ resetIfBuilt();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+ DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+ return setPubliclyVisibleSchema(factory.getSchemaName(), packageIdentifier);
+ }
+// @exportToFramework:endStrip()
+
+ /**
+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+ *
+ * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold
+ * required permissions AND it is a specified package.
+ *
+ * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+ * and the querier will have access if they match ANY of the
+ * {@link SchemaVisibilityConfig}.
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all requirements
+ * that a call must to match to access the schema.
+ */
+ // Merged list available from getSchemasVisibleToConfigs
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+ @NonNull
+ public Builder addSchemaTypeVisibleToConfig(@NonNull String schemaType,
+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(schemaVisibilityConfig);
+ resetIfBuilt();
+ Set<SchemaVisibilityConfig> visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType);
+ if (visibleToConfigs == null) {
+ visibleToConfigs = new ArraySet<>();
+ mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs);
+ }
+ visibleToConfigs.add(schemaVisibilityConfig);
+ return this;
+ }
+
+ /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
+ @CanIgnoreReturnValue
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+ @NonNull
+ public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ resetIfBuilt();
+ mSchemaVisibleToConfigs.remove(schemaType);
+ return this;
+ }
+
+ /**
* Sets the {@link Migrator} associated with the given SchemaType.
*
* <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
@@ -684,6 +871,12 @@
*
* <p>By default, app data sharing between applications is disabled.
*
+ * <p> The relationship between visible packages added in this method and permission
+ * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The
+ * caller could access the schema if they match ANY requirements. If you want to set
+ * "AND" requirements like a caller must hold required permissions AND it is a specified
+ * package, please use {@link #addSchemaTypeVisibleToConfig}.
+ *
* <p>Merged list available from {@link #getSchemasVisibleToPackages()}.
*
* @param documentClass The {@link androidx.appsearch.annotation.Document} class to set
@@ -721,6 +914,12 @@
* {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
* {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
*
+ * <p> The relationship between visible packages added in this method and permission
+ * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The
+ * caller could access the schema if they match ANY requirements. If you want to set
+ * "AND" requirements like a caller must hold required permissions AND it is a specified
+ * package, please use {@link #addSchemaTypeVisibleToConfig}.
+ *
* <p>Merged map available from {@link #getRequiredPermissionsForSchemaTypeVisibility()}.
* @see android.Manifest.permission#READ_SMS
* @see android.Manifest.permission#READ_CALENDAR
@@ -768,6 +967,58 @@
DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName());
}
+
+ /**
+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+ *
+ * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold
+ * required permissions AND it is a specified package.
+ *
+ * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+ * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}.
+ *
+ * @param documentClass A class annotated with
+ * {@link androidx.appsearch.annotation.Document}, the
+ * visibility of which will be configured
+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all
+ * requirements that a call must to match to access the
+ * schema.
+ */
+ // Merged list available from getSchemasVisibleToConfigs
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @NonNull
+ public Builder addDocumentClassVisibleToConfig(
+ @NonNull Class<?> documentClass,
+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig)
+ throws AppSearchException {
+ Preconditions.checkNotNull(documentClass);
+ resetIfBuilt();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+ DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+ return addSchemaTypeVisibleToConfig(factory.getSchemaName(),
+ schemaVisibilityConfig);
+ }
+
+ /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+ @NonNull
+ public Builder clearDocumentClassVisibleToConfigs(
+ @NonNull Class<?> documentClass) throws AppSearchException {
+ Preconditions.checkNotNull(documentClass);
+ resetIfBuilt();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+ DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+ return clearSchemaTypeVisibleToConfigs(factory.getSchemaName());
+ }
// @exportToFramework:endStrip()
/**
@@ -841,6 +1092,8 @@
Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
+ referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet());
+ referencedSchemas.addAll(mSchemaVisibleToConfigs.keySet());
for (AppSearchSchema schema : mSchemas) {
referencedSchemas.remove(schema.getSchemaType());
@@ -861,6 +1114,8 @@
mSchemasNotDisplayedBySystem,
mSchemasVisibleToPackages,
mSchemasVisibleToPermissions,
+ mPubliclyVisibleSchemas,
+ mSchemaVisibleToConfigs,
mMigrators,
mForceOverride,
mVersion);
@@ -876,8 +1131,18 @@
}
mSchemasVisibleToPackages = schemasVisibleToPackages;
+ mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas);
+
mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
+ ArrayMap<String, Set<SchemaVisibilityConfig>> schemaVisibleToConfigs =
+ new ArrayMap<>(mSchemaVisibleToConfigs.size());
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ mSchemaVisibleToConfigs.entrySet()) {
+ schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ mSchemaVisibleToConfigs = schemaVisibleToConfigs;
+
mSchemas = new ArraySet<>(mSchemas);
mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
mMigrators = new ArrayMap<>(mMigrators);
@@ -886,8 +1151,8 @@
}
}
- static ArrayMap<String, Set<Set<Integer>>> deepCopy(@NonNull Map<String,
- Set<Set<Integer>>> original) {
+ private 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<>();
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 07d087a..85866fd 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -16,12 +16,19 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.MigrationFailureCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.SetSchemaResponseCreator;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -32,57 +39,70 @@
import java.util.Set;
/** The response class of {@link AppSearchSession#setSchemaAsync} */
-public class SetSchemaResponse {
[email protected](creator = "SetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SetSchemaResponse extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull public static final Parcelable.Creator<SetSchemaResponse> CREATOR =
+ new SetSchemaResponseCreator();
- private static final String DELETED_TYPES_FIELD = "deletedTypes";
- private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
- private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
+ @Field(id = 1)
+ final List<String> mDeletedTypes;
+ @Field(id = 2)
+ final List<String> mIncompatibleTypes;
+ @Field(id = 3)
+ final List<String> mMigratedTypes;
- private final Bundle mBundle;
/**
- * The migrationFailures won't be saved in the bundle. Since:
+ * The migrationFailures won't be saved as a SafeParcelable field. Since:
* <ul>
* <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
- * the SDK side in platform. We don't need to pass it from service side via binder.
- * <li>Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
- * back in constructor will be a huge waste.
+ * the SDK side in platform. We don't need to pass it from service side via binder as
+ * a part of {@link SetSchemaResponse}.
+ * <li>Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and
+ * then back in constructor will be a huge waste.
* </ul>
*/
private final List<MigrationFailure> mMigrationFailures;
- /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */
- @Nullable
- private Set<String> mDeletedTypes;
+ /** Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use
+ */
+ @Nullable private Set<String> mDeletedTypesCached;
- /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */
- @Nullable
- private Set<String> mMigratedTypes;
+ /** Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first
+ * use.
+ */
+ @Nullable private Set<String> mMigratedTypesCached;
/**
- * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.
+ * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at
+ * first use.
*/
- @Nullable
- private Set<String> mIncompatibleTypes;
+ @Nullable private Set<String> mIncompatibleTypesCached;
- SetSchemaResponse(@NonNull Bundle bundle, @NonNull List<MigrationFailure> migrationFailures) {
- mBundle = Preconditions.checkNotNull(bundle);
+ @Constructor
+ SetSchemaResponse(
+ @Param(id = 1) @NonNull List<String> deletedTypes,
+ @Param(id = 2) @NonNull List<String> incompatibleTypes,
+ @Param(id = 3) @NonNull List<String> migratedTypes) {
+ mDeletedTypes = deletedTypes;
+ mIncompatibleTypes = incompatibleTypes;
+ mMigratedTypes = migratedTypes;
+ mMigrationFailures = Collections.emptyList();
+ }
+
+ SetSchemaResponse(
+ @NonNull List<String> deletedTypes,
+ @NonNull List<String> incompatibleTypes,
+ @NonNull List<String> migratedTypes,
+ @NonNull List<MigrationFailure> migrationFailures) {
+ mDeletedTypes = deletedTypes;
+ mIncompatibleTypes = incompatibleTypes;
+ mMigratedTypes = migratedTypes;
mMigrationFailures = Preconditions.checkNotNull(migrationFailures);
}
- SetSchemaResponse(@NonNull Bundle bundle) {
- this(bundle, /*migrationFailures=*/ Collections.emptyList());
- }
-
- /**
- * Returns the {@link Bundle} populated by this builder.
- * @exportToFramework:hide
- */
- @NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Bundle getBundle() {
- return mBundle;
- }
-
/**
* Returns a {@link List} of all failed {@link MigrationFailure}.
*
@@ -109,11 +129,10 @@
*/
@NonNull
public Set<String> getDeletedTypes() {
- if (mDeletedTypes == null) {
- mDeletedTypes = new ArraySet<>(
- Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD)));
+ if (mDeletedTypesCached == null) {
+ mDeletedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mDeletedTypes));
}
- return Collections.unmodifiableSet(mDeletedTypes);
+ return Collections.unmodifiableSet(mDeletedTypesCached);
}
/**
@@ -131,11 +150,10 @@
*/
@NonNull
public Set<String> getMigratedTypes() {
- if (mMigratedTypes == null) {
- mMigratedTypes = new ArraySet<>(
- Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));
+ if (mMigratedTypesCached == null) {
+ mMigratedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mMigratedTypes));
}
- return Collections.unmodifiableSet(mMigratedTypes);
+ return Collections.unmodifiableSet(mMigratedTypesCached);
}
/**
@@ -151,27 +169,11 @@
*/
@NonNull
public Set<String> getIncompatibleTypes() {
- if (mIncompatibleTypes == null) {
- mIncompatibleTypes = new ArraySet<>(
- Preconditions.checkNotNull(
- mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));
+ if (mIncompatibleTypesCached == null) {
+ mIncompatibleTypesCached =
+ new ArraySet<>(Preconditions.checkNotNull(mIncompatibleTypes));
}
- return Collections.unmodifiableSet(mIncompatibleTypes);
- }
-
- /**
- * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy
- public Builder toBuilder() {
- return new Builder()
- .addDeletedTypes(getDeletedTypes())
- .addIncompatibleTypes(getIncompatibleTypes())
- .addMigratedTypes(getMigratedTypes())
- .addMigrationFailures(mMigrationFailures);
+ return Collections.unmodifiableSet(mIncompatibleTypesCached);
}
/** Builder for {@link SetSchemaResponse} objects. */
@@ -182,6 +184,23 @@
private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
private boolean mBuilt = false;
+ /**
+ * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse.
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder(@NonNull SetSchemaResponse setSchemaResponse) {
+ Preconditions.checkNotNull(setSchemaResponse);
+ mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes());
+ mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes());
+ mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes());
+ mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures());
+ }
+
+ /** Create a {@link Builder} object} */
+ public Builder() {}
+
/** Adds {@link MigrationFailure}s to the list of migration failures. */
@CanIgnoreReturnValue
@NonNull
@@ -266,15 +285,15 @@
/** Builds a {@link SetSchemaResponse} object. */
@NonNull
public SetSchemaResponse build() {
- Bundle bundle = new Bundle();
- bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);
- bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);
- bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes);
mBuilt = true;
// Avoid converting the potential thousands of MigrationFailures to Pracelable and
// back just for put in bundle. In platform, we should set MigrationFailures in
// AppSearchSession after we pass SetSchemaResponse via binder.
- return new SetSchemaResponse(bundle, mMigrationFailures);
+ return new SetSchemaResponse(
+ mDeletedTypes,
+ mIncompatibleTypes,
+ mMigratedTypes,
+ mMigrationFailures);
}
private void resetIfBuilt() {
@@ -288,18 +307,50 @@
}
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ SetSchemaResponseCreator.writeToParcel(this, dest, flags);
+ }
+
/**
* The class represents a post-migrated {@link GenericDocument} that failed to be saved by
* {@link AppSearchSession#setSchemaAsync}.
*/
- public static class MigrationFailure {
- private static final String SCHEMA_TYPE_FIELD = "schemaType";
- private static final String NAMESPACE_FIELD = "namespace";
- private static final String DOCUMENT_ID_FIELD = "id";
- private static final String ERROR_MESSAGE_FIELD = "errorMessage";
- private static final String RESULT_CODE_FIELD = "resultCode";
+ @SafeParcelable.Class(creator = "MigrationFailureCreator")
+ @SuppressWarnings("HiddenSuperclass")
+ public static class MigrationFailure extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<MigrationFailure> CREATOR =
+ new MigrationFailureCreator();
- private final Bundle mBundle;
+ @Field(id = 1, getter = "getNamespace")
+ private final String mNamespace;
+ @Field(id = 2, getter = "getDocumentId")
+ private final String mDocumentId;
+ @Field(id = 3, getter = "getSchemaType")
+ private final String mSchemaType;
+ @Field(id = 4)
+ @Nullable final String mErrorMessage;
+ @Field(id = 5)
+ final int mResultCode;
+
+ @Constructor
+ MigrationFailure(
+ @Param(id = 1) @NonNull String namespace,
+ @Param(id = 2) @NonNull String documentId,
+ @Param(id = 3) @NonNull String schemaType,
+ @Param(id = 4) @Nullable String errorMessage,
+ @Param(id = 5) int resultCode) {
+ mNamespace = namespace;
+ mDocumentId = documentId;
+ mSchemaType = schemaType;
+ mErrorMessage = errorMessage;
+ mResultCode = resultCode;
+ }
/**
* Constructs a new {@link MigrationFailure}.
@@ -315,49 +366,33 @@
@NonNull String documentId,
@NonNull String schemaType,
@NonNull AppSearchResult<?> failedResult) {
- mBundle = new Bundle();
- mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace));
- mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId));
- mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType));
+ mNamespace = namespace;
+ mDocumentId = documentId;
+ mSchemaType = schemaType;
Preconditions.checkNotNull(failedResult);
Preconditions.checkArgument(
!failedResult.isSuccess(), "failedResult was actually successful");
- mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());
- mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());
- }
-
- MigrationFailure(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
-
- /**
- * Returns the Bundle of the {@link MigrationFailure}.
- *
- * @exportToFramework:hide
- */
- @NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Bundle getBundle() {
- return mBundle;
+ mErrorMessage = failedResult.getErrorMessage();
+ mResultCode = failedResult.getResultCode();
}
/** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
@NonNull
public String getNamespace() {
- return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/"");
+ return mNamespace;
}
/** Returns the id of the {@link GenericDocument} that failed to be migrated. */
@NonNull
public String getDocumentId() {
- return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/"");
+ return mDocumentId;
}
/** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
@NonNull
public String getSchemaType() {
- return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/"");
+ return mSchemaType;
}
/**
@@ -366,8 +401,7 @@
*/
@NonNull
public AppSearchResult<Void> getAppSearchResult() {
- return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD),
- mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/""));
+ return AppSearchResult.newFailedResult(mResultCode, mErrorMessage);
}
@NonNull
@@ -377,5 +411,12 @@
+ getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: "
+ getAppSearchResult().toString() + "}";
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ MigrationFailureCreator.writeToParcel(this, dest, flags);
+ }
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
index d95eff6..fd22846 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -16,39 +16,49 @@
package androidx.appsearch.app;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.StorageInfoCreator;
/** The response class of {@code AppSearchSession#getStorageInfo}. */
-public class StorageInfo {
-
- private static final String SIZE_BYTES_FIELD = "sizeBytes";
- private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
- private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
-
- private final Bundle mBundle;
-
- StorageInfo(@NonNull Bundle bundle) {
- mBundle = Preconditions.checkNotNull(bundle);
- }
-
- /**
- * Returns the {@link Bundle} populated by this builder.
- * @exportToFramework:hide
- */
- @NonNull
[email protected](creator = "StorageInfoCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class StorageInfo extends AbstractSafeParcelable {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public Bundle getBundle() {
- return mBundle;
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<StorageInfo> CREATOR = new StorageInfoCreator();
+
+ @Field(id = 1, getter = "getSizeBytes")
+ private long mSizeBytes;
+
+ @Field(id = 2, getter = "getAliveDocumentsCount")
+ private int mAliveDocumentsCount;
+
+ @Field(id = 3, getter = "getAliveNamespacesCount")
+ private int mAliveNamespacesCount;
+
+ @Constructor
+ StorageInfo(
+ @Param(id = 1) long sizeBytes,
+ @Param(id = 2) int aliveDocumentsCount,
+ @Param(id = 3) int aliveNamespacesCount) {
+ mSizeBytes = sizeBytes;
+ mAliveDocumentsCount = aliveDocumentsCount;
+ mAliveNamespacesCount = aliveNamespacesCount;
}
/** Returns the estimated size of the session's database in bytes. */
public long getSizeBytes() {
- return mBundle.getLong(SIZE_BYTES_FIELD);
+ return mSizeBytes;
}
/**
@@ -58,7 +68,7 @@
* set in {@link GenericDocument.Builder#setTtlMillis}.
*/
public int getAliveDocumentsCount() {
- return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
+ return mAliveDocumentsCount;
}
/**
@@ -69,7 +79,7 @@
* set in {@link GenericDocument.Builder#setTtlMillis}.
*/
public int getAliveNamespacesCount() {
- return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
+ return mAliveNamespacesCount;
}
/** Builder for {@link StorageInfo} objects. */
@@ -105,11 +115,14 @@
/** Builds a {@link StorageInfo} object. */
@NonNull
public StorageInfo build() {
- Bundle bundle = new Bundle();
- bundle.putLong(SIZE_BYTES_FIELD, mSizeBytes);
- bundle.putInt(ALIVE_DOCUMENTS_COUNT, mAliveDocumentsCount);
- bundle.putInt(ALIVE_NAMESPACES_COUNT, mAliveNamespacesCount);
- return new StorageInfo(bundle);
+ return new StorageInfo(mSizeBytes, mAliveDocumentsCount, mAliveNamespacesCount);
}
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ StorageInfoCreator.writeToParcel(this, dest, flags);
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
deleted file mode 100644
index 2bea91f..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
+++ /dev/null
@@ -1,446 +0,0 @@
-/*
- * 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.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.safeparcel.AbstractSafeParcelable;
-import androidx.appsearch.safeparcel.SafeParcelable;
-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityDocumentCreator;
-import androidx.collection.ArraySet;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * Holds the visibility settings that apply to a schema type.
- * @exportToFramework:hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityDocumentCreator")
-public final class VisibilityDocument extends AbstractSafeParcelable {
- @NonNull
- public static final VisibilityDocumentCreator CREATOR = new VisibilityDocumentCreator();
-
- /**
- * The Schema type for documents that hold AppSearch's metadata, such as 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();
-
- @NonNull
- @Field(id = 1, getter = "getId")
- private String mId;
-
- @Field(id = 2, getter = "isNotDisplayedBySystem")
- private final boolean mIsNotDisplayedBySystem;
-
- @NonNull
- @Field(id = 3, getter = "getPackageNames")
- private final String[] mPackageNames;
-
- @NonNull
- @Field(id = 4, getter = "getSha256Certs")
- private final byte[][] mSha256Certs;
-
- @Nullable
- @Field(id = 5, getter = "getPermissionDocuments")
- private final VisibilityPermissionDocument[] mPermissionDocuments;
-
- @Nullable
- // We still need to convert this class to a GenericDocument until we completely treat it
- // differently in AppSearchImpl.
- // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
- // visibility information.
- private GenericDocument mGenericDocument;
-
- @Nullable
- private Integer mHashCode;
-
- @Constructor
- VisibilityDocument(
- @Param(id = 1) @NonNull String id,
- @Param(id = 2) boolean isNotDisplayedBySystem,
- @Param(id = 3) @NonNull String[] packageNames,
- @Param(id = 4) @NonNull byte[][] sha256Certs,
- @Param(id = 5) @Nullable VisibilityPermissionDocument[] permissionDocuments) {
- mId = Objects.requireNonNull(id);
- mIsNotDisplayedBySystem = isNotDisplayedBySystem;
- mPackageNames = Objects.requireNonNull(packageNames);
- mSha256Certs = Objects.requireNonNull(sha256Certs);
- mPermissionDocuments = permissionDocuments;
- }
-
- /**
- * Gets the id for this VisibilityDocument.
- *
- * <p>This is being used as the document id when we convert a {@link VisibilityDocument}
- * to a {@link GenericDocument}.
- */
- @NonNull
- public String getId() {
- return mId;
- }
-
- /** Returns whether this schema is visible to the system. */
- public boolean isNotDisplayedBySystem() {
- return mIsNotDisplayedBySystem;
- }
-
- /**
- * 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 mPackageNames;
- }
-
- /**
- * 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 mSha256Certs;
- }
-
- /** Gets a list of {@link VisibilityDocument}.
- *
- * <p>A {@link VisibilityDocument} holds all required permissions for the caller need to have
- * to access the schema this {@link VisibilityDocument} presents.
- */
- @Nullable
- VisibilityPermissionDocument[] getPermissionDocuments() {
- return mPermissionDocuments;
- }
-
- /**
- * Returns an array of Android Permissions that caller mush hold to access the schema this
- * {@link VisibilityDocument} represents.
- */
- @NonNull
- public Set<Set<Integer>> getVisibleToPermissions() {
- if (mPermissionDocuments == null) {
- return Collections.emptySet();
- }
- Set<Set<Integer>> visibleToPermissions = new ArraySet<>(mPermissionDocuments.length);
- for (VisibilityPermissionDocument permissionDocument : mPermissionDocuments) {
- Set<Integer> requiredPermissions = permissionDocument.getAllRequiredPermissions();
- if (requiredPermissions != null) {
- visibleToPermissions.add(requiredPermissions);
- }
- }
- return visibleToPermissions;
- }
-
- /** 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;
- }
-
- /**
- * Generates a {@link GenericDocument} from the current class.
- *
- * <p>This conversion is needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.
- */
- @NonNull
- public GenericDocument toGenericDocument() {
- if (mGenericDocument == null) {
- GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
- NAMESPACE, mId, SCHEMA_TYPE);
- builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, mIsNotDisplayedBySystem);
- builder.setPropertyString(PACKAGE_NAME_PROPERTY, mPackageNames);
- builder.setPropertyBytes(SHA_256_CERT_PROPERTY, mSha256Certs);
-
- // Generate an array of GenericDocument for VisibilityPermissionDocument.
- if (mPermissionDocuments != null) {
- GenericDocument[] permissionGenericDocs =
- new GenericDocument[mPermissionDocuments.length];
- for (int i = 0; i < mPermissionDocuments.length; ++i) {
- permissionGenericDocs[i] = mPermissionDocuments[i].toGenericDocument();
- }
- builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);
- }
-
- // The creationTimestamp doesn't matter for Visibility documents.
- // But to make tests pass, we set it 0 so two GenericDocuments generated from
- // the same VisibilityDocument can be same.
- builder.setCreationTimestampMillis(0L);
-
- mGenericDocument = builder.build();
- }
- return mGenericDocument;
- }
-
- @Override
- public int hashCode() {
- if (mHashCode == null) {
- mHashCode = Objects.hash(
- mId,
- mIsNotDisplayedBySystem,
- Arrays.hashCode(mPackageNames),
- Arrays.deepHashCode(mSha256Certs),
- Arrays.hashCode(mPermissionDocuments));
- }
- return mHashCode;
- }
-
- @Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof VisibilityDocument)) {
- return false;
- }
- VisibilityDocument otherVisibilityDocument = (VisibilityDocument) other;
- return mId.equals(otherVisibilityDocument.mId)
- && mIsNotDisplayedBySystem == otherVisibilityDocument.mIsNotDisplayedBySystem
- && Arrays.equals(
- mPackageNames, otherVisibilityDocument.mPackageNames)
- && Arrays.deepEquals(
- mSha256Certs, otherVisibilityDocument.mSha256Certs)
- && Arrays.equals(
- mPermissionDocuments, otherVisibilityDocument.mPermissionDocuments);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- VisibilityDocumentCreator.writeToParcel(this, dest, flags);
- }
-
- /** Builder for {@link VisibilityDocument}. */
- public static final class Builder {
- private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
- private String mId;
- private boolean mIsNotDisplayedBySystem;
- private VisibilityPermissionDocument[] mPermissionDocuments;
-
- /**
- * 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) {
- mId = Objects.requireNonNull(id);
- }
-
- /**
- * Constructs a {@link VisibilityDocument} from a {@link GenericDocument}.
- *
- * <p>This constructor is still needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.
- */
- public Builder(@NonNull GenericDocument genericDocument) {
- Objects.requireNonNull(genericDocument);
-
- mId = genericDocument.getId();
- mIsNotDisplayedBySystem = genericDocument.getPropertyBoolean(
- NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
-
- String[] packageNames = genericDocument.getPropertyStringArray(PACKAGE_NAME_PROPERTY);
- byte[][] sha256Certs = genericDocument.getPropertyBytesArray(SHA_256_CERT_PROPERTY);
- for (int i = 0; i < packageNames.length; ++i) {
- mPackageIdentifiers.add(new PackageIdentifier(packageNames[i], sha256Certs[i]));
- }
-
- GenericDocument[] permissionDocs =
- genericDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);
- if (permissionDocs != null) {
- mPermissionDocuments = new VisibilityPermissionDocument[permissionDocs.length];
- for (int i = 0; i < permissionDocs.length; ++i) {
- mPermissionDocuments[i] = new VisibilityPermissionDocument.Builder(
- permissionDocs[i]).build();
- }
- }
- }
-
- public Builder(@NonNull VisibilityDocument visibilityDocument) {
- Objects.requireNonNull(visibilityDocument);
-
- mIsNotDisplayedBySystem = visibilityDocument.mIsNotDisplayedBySystem;
- mPermissionDocuments = visibilityDocument.mPermissionDocuments;
- for (int i = 0; i < visibilityDocument.mPackageNames.length; ++i) {
- mPackageIdentifiers.add(new PackageIdentifier(visibilityDocument.mPackageNames[i],
- visibilityDocument.mSha256Certs[i]));
- }
- }
-
- /** Sets id. */
- @CanIgnoreReturnValue
- @NonNull
- public Builder setId(@NonNull String id) {
- mId = Objects.requireNonNull(id);
- return this;
- }
-
- /** Sets whether this schema has opted out of platform surfacing. */
- @CanIgnoreReturnValue
- @NonNull
- public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
- mIsNotDisplayedBySystem = notDisplayedBySystem;
- return this;
- }
-
- /** Add {@link PackageIdentifier} of packages which has access to this schema. */
- @CanIgnoreReturnValue
- @NonNull
- public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
- mPackageIdentifiers.addAll(Objects.requireNonNull(packageIdentifiers));
- return this;
- }
-
- /** Add {@link PackageIdentifier} of packages which has access to this schema. */
- @CanIgnoreReturnValue
- @NonNull
- public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
- mPackageIdentifiers.add(Objects.requireNonNull(packageIdentifier));
- return this;
- }
-
- /**
- * Sets 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.
- */
- @CanIgnoreReturnValue
- @NonNull
- public Builder setVisibleToPermissions(@NonNull Set<Set<Integer>> visibleToPermissions) {
- Objects.requireNonNull(visibleToPermissions);
- mPermissionDocuments =
- new VisibilityPermissionDocument[visibleToPermissions.size()];
- int i = 0;
- for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
- mPermissionDocuments[i++] =
- new VisibilityPermissionDocument.Builder(
- NAMESPACE, /*id=*/ String.valueOf(i))
- .setVisibleToAllRequiredPermissions(allRequiredPermissions)
- .build();
- }
- return this;
- }
-
- /** Build a {@link VisibilityDocument} */
- @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;
- }
- return new VisibilityDocument(mId, mIsNotDisplayedBySystem,
- packageNames, sha256Certs, mPermissionDocuments);
- }
- }
-}
-
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
new file mode 100644
index 0000000..40e0f81
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2023 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The config class that holds all required permissions for a caller need to hold to access the
+ * schema which the outer {@link SchemaVisibilityConfig} represents.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityPermissionConfigCreator")
+public final class VisibilityPermissionConfig extends AbstractSafeParcelable {
+ @NonNull
+ public static final Parcelable.Creator<VisibilityPermissionConfig> CREATOR =
+ new VisibilityPermissionConfigCreator();
+
+ /**
+ * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
+ */
+ public static final String SCHEMA_TYPE = "VisibilityPermissionType";
+
+ /** Property that holds the required permissions to access the schema. */
+ public static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update schema version number in
+ * VisibilityToDocumentConverter
+ */
+ 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();
+
+ @Nullable
+ @Field(id = 1)
+ final int[] mAllRequiredPermissions;
+
+ @Nullable
+ // We still need to convert this class to a GenericDocument until we completely treat it
+ // differently in AppSearchImpl.
+ // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
+ // visibility information.
+ private GenericDocument mGenericDocument;
+
+ @Nullable
+ private Integer mHashCode;
+
+ @Constructor
+ VisibilityPermissionConfig(@Param(id = 1) @Nullable int[] allRequiredPermissions) {
+ mAllRequiredPermissions = allRequiredPermissions;
+ }
+
+ /**
+ * Sets a set of Android Permissions that caller must hold to access the schema that the
+ * outer {@link SchemaVisibilityConfig} represents.
+ */
+ public VisibilityPermissionConfig(@NonNull Set<Integer> allRequiredPermissions) {
+ mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema that the
+ * outer {@link SchemaVisibilityConfig} represents.
+ */
+ @Nullable
+ public Set<Integer> getAllRequiredPermissions() {
+ return toIntegerSet(mAllRequiredPermissions);
+ }
+
+ @NonNull
+ private static int[] toInts(@NonNull Set<Integer> properties) {
+ int[] outputs = new int[properties.size()];
+ int i = 0;
+ for (int property : properties) {
+ outputs[i++] = property;
+ }
+ return outputs;
+ }
+
+ @Nullable
+ private static Set<Integer> toIntegerSet(@Nullable int[] properties) {
+ if (properties == null) {
+ return null;
+ }
+ Set<Integer> outputs = new ArraySet<>(properties.length);
+ for (int property : properties) {
+ outputs.add(property);
+ }
+ return outputs;
+ }
+
+ /**
+ * Generates a {@link GenericDocument} from the current class.
+ *
+ * <p>This conversion is needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.
+ */
+ @NonNull
+ public GenericDocument toGenericDocument() {
+ if (mGenericDocument == null) {
+ // This is used as a nested document, we do not need a namespace or id.
+ GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
+ /*namespace=*/"", /*id=*/"", SCHEMA_TYPE);
+
+ if (mAllRequiredPermissions != null) {
+ // GenericDocument only supports long, so int[] needs to be converted to
+ // long[] here.
+ long[] longs = new long[mAllRequiredPermissions.length];
+ for (int i = 0; i < mAllRequiredPermissions.length; ++i) {
+ longs[i] = mAllRequiredPermissions[i];
+ }
+ builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);
+ }
+
+ // The creationTimestamp doesn't matter for Visibility documents.
+ // But to make tests pass, we set it 0 so two GenericDocuments generated from
+ // the same VisibilityPermissionConfig can be same.
+ builder.setCreationTimestampMillis(0L);
+
+ mGenericDocument = builder.build();
+ }
+ return mGenericDocument;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = Arrays.hashCode(mAllRequiredPermissions);
+ }
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof VisibilityPermissionConfig)) {
+ return false;
+ }
+ VisibilityPermissionConfig otherVisibilityPermissionConfig =
+ (VisibilityPermissionConfig) other;
+ return Arrays.equals(mAllRequiredPermissions,
+ otherVisibilityPermissionConfig.mAllRequiredPermissions);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ VisibilityPermissionConfigCreator.writeToParcel(this, dest, flags);
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
deleted file mode 100644
index 54269fd..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * 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 android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.safeparcel.AbstractSafeParcelable;
-import androidx.appsearch.safeparcel.SafeParcelable;
-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionDocumentCreator;
-import androidx.collection.ArraySet;
-
-import java.util.Arrays;
-import java.util.Objects;
-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.
- * @exportToFramework:hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityPermissionDocumentCreator")
-public final class VisibilityPermissionDocument extends AbstractSafeParcelable {
- @NonNull
- public static final VisibilityPermissionDocumentCreator CREATOR =
- new VisibilityPermissionDocumentCreator();
-
- /**
- * The Schema type for documents that hold AppSearch's metadata, such as 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();
-
- @NonNull
- @Field(id = 1, getter = "getId")
- private final String mId;
-
- @NonNull
- @Field(id = 2, getter = "getNamespace")
- private final String mNamespace;
-
- @Nullable
- @Field(id = 3, getter = "getAllRequiredPermissionsInts")
- // SafeParcelable doesn't support Set<Integer>, so we have to convert it to int[].
- private final int[] mAllRequiredPermissions;
-
- @Nullable
- // We still need to convert this class to a GenericDocument until we completely treat it
- // differently in AppSearchImpl.
- // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
- // visibility information.
- private GenericDocument mGenericDocument;
-
- @Nullable
- private Integer mHashCode;
-
- @Constructor
- VisibilityPermissionDocument(
- @Param(id = 1) @NonNull String id,
- @Param(id = 2) @NonNull String namespace,
- @Param(id = 3) @Nullable int[] allRequiredPermissions) {
- mId = Objects.requireNonNull(id);
- mNamespace = Objects.requireNonNull(namespace);
- mAllRequiredPermissions = allRequiredPermissions;
- }
-
- /**
- * Gets the id for this {@link VisibilityPermissionDocument}.
- *
- * <p>This is being used as the document id when we convert a
- * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.
- */
- @NonNull
- public String getId() {
- return mId;
- }
-
- /**
- * Gets the namespace for this {@link VisibilityPermissionDocument}.
- *
- * <p>This is being used as the namespace when we convert a
- * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.
- */
- @NonNull
- public String getNamespace() {
- return mNamespace;
- }
-
- /** Gets the required Android Permissions in an int array. */
- @Nullable
- int[] getAllRequiredPermissionsInts() {
- return mAllRequiredPermissions;
- }
-
- /**
- * 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 toIntegerSet(mAllRequiredPermissions);
- }
-
- @NonNull
- private static int[] toInts(@NonNull Set<Integer> properties) {
- int[] outputs = new int[properties.size()];
- int i = 0;
- for (int property : properties) {
- outputs[i++] = property;
- }
- return outputs;
- }
-
- @Nullable
- private static Set<Integer> toIntegerSet(@Nullable int[] properties) {
- if (properties == null) {
- return null;
- }
- Set<Integer> outputs = new ArraySet<>(properties.length);
- for (int property : properties) {
- outputs.add(property);
- }
- return outputs;
- }
-
- /**
- * Generates a {@link GenericDocument} from the current class.
- *
- * <p>This conversion is needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.
- */
- @NonNull
- public GenericDocument toGenericDocument() {
- if (mGenericDocument == null) {
- GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
- mNamespace, mId, SCHEMA_TYPE);
-
- if (mAllRequiredPermissions != null) {
- // GenericDocument only supports long, so int[] needs to be converted to
- // long[] here.
- long[] longs = new long[mAllRequiredPermissions.length];
- for (int i = 0; i < mAllRequiredPermissions.length; ++i) {
- longs[i] = mAllRequiredPermissions[i];
- }
- builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);
- }
-
- mGenericDocument = builder.build();
- }
- return mGenericDocument;
- }
-
- @Override
- public int hashCode() {
- if (mHashCode == null) {
- mHashCode = Objects.hash(mId, mNamespace, Arrays.hashCode(mAllRequiredPermissions));
- }
- return mHashCode;
- }
-
- @Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof VisibilityPermissionDocument)) {
- return false;
- }
- VisibilityPermissionDocument otherVisibilityPermissionDocument =
- (VisibilityPermissionDocument) other;
- return mId.equals(otherVisibilityPermissionDocument.mId)
- && mNamespace.equals(otherVisibilityPermissionDocument.mNamespace)
- && Arrays.equals(
- mAllRequiredPermissions,
- otherVisibilityPermissionDocument.mAllRequiredPermissions);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- VisibilityPermissionDocumentCreator.writeToParcel(this, dest, flags);
- }
-
- /** Builder for {@link VisibilityPermissionDocument}. */
- public static final class Builder {
- private String mId;
- private String mNamespace;
- private int[] mAllRequiredPermissions;
-
- /**
- * Constructs a {@link VisibilityPermissionDocument} from a {@link GenericDocument}.
- *
- * <p>This constructor is still needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.
- */
- public Builder(@NonNull GenericDocument genericDocument) {
- Objects.requireNonNull(genericDocument);
- mId = genericDocument.getId();
- mNamespace = genericDocument.getNamespace();
- // GenericDocument only supports long[], so we need to convert it back to int[].
- long[] longs = genericDocument.getPropertyLongArray(
- ALL_REQUIRED_PERMISSIONS_PROPERTY);
- if (longs != null) {
- mAllRequiredPermissions = new int[longs.length];
- for (int i = 0; i < longs.length; ++i) {
- mAllRequiredPermissions[i] = (int) longs[i];
- }
- }
- }
-
- /** Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}. */
- public Builder(@NonNull String namespace, @NonNull String id) {
- mNamespace = Objects.requireNonNull(namespace);
- mId = Objects.requireNonNull(id);
- }
-
- /**
- * Sets a set of Android Permissions that caller mush hold to access the schema that the
- * outer {@link VisibilityDocument} represents.
- */
- @CanIgnoreReturnValue
- @NonNull
- public Builder setVisibleToAllRequiredPermissions(
- @NonNull Set<Integer> allRequiredPermissions) {
- mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));
- return this;
- }
-
- /** Builds a {@link VisibilityPermissionDocument} */
- @NonNull
- public VisibilityPermissionDocument build() {
- return new VisibilityPermissionDocument(mId,
- mNamespace,
- mAllRequiredPermissions);
- }
- }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
new file mode 100644
index 0000000..4fb00e1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.checker.initialization.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnderInitialization {
+
+ // These fields maintain API compatibility with annotations that expect arguments.
+
+ String[] value() default {};
+
+ boolean result() default false;
+
+ String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
new file mode 100644
index 0000000..8faa0dd
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.checker.initialization.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnknownInitialization {
+
+ // These fields maintain API compatibility with annotations that expect arguments.
+
+ String[] value() default {};
+
+ boolean result() default false;
+
+ String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
new file mode 100644
index 0000000..c9137c5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.checker.nullness.qual;
+
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface Nullable {
+
+ // These fields maintain API compatibility with annotations that expect arguments.
+
+ String[] value() default {};
+
+ boolean result() default false;
+
+ String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
new file mode 100644
index 0000000..4c39b9b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.checker.nullness.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface RequiresNonNull {
+
+ // These fields maintain API compatibility with annotations that expect arguments.
+
+ String[] value() default {};
+
+ boolean result() default false;
+
+ String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
index 2930d2d..b39435f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
@@ -27,7 +27,7 @@
* for propagating to the client.
*/
public class AppSearchException extends Exception {
- private final @AppSearchResult.ResultCode int mResultCode;
+ @AppSearchResult.ResultCode private final int mResultCode;
/**
* Initializes an {@link AppSearchException} with no message.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
similarity index 60%
copy from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
copy to appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
index 5138d2d..693be73 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
@@ -14,27 +14,18 @@
* limitations under the License.
*/
-package androidx.pdf.pdflib;
+package androidx.appsearch.flags;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.pdf.models.PdfDocumentRemote;
/**
+ * Indicates an API is part of a feature that is guarded by an aconfig flag in the framework, and
+ * only available if the flag is enabled.
*
+ * <p>Our own Jetpack version is created here for code sync purpose.
*/
+// @exportToFramework:skipFile()
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentRemoteProto {
- private PdfDocumentRemote mRemote;
-
- public PdfDocumentRemoteProto(@NonNull PdfDocumentRemote remote) {
- this.mRemote = remote;
- }
-
- @NonNull
- public PdfDocumentRemote getPdfDocumentRemote() {
- return mRemote;
- }
-
- // TODO: Add goto links methods from the original kotlin file
+public @interface FlaggedApi {
+ String value();
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
new file mode 100644
index 0000000..5ad11f3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+
+import java.util.Collection;
+
+/**
+ * Flags to control different features.
+ *
+ * <p>In Jetpack, those values can't be changed during runtime.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Flags {
+ private Flags() {
+ }
+
+ // The prefix of all the flags defined for AppSearch. The prefix has
+ // "com.android.appsearch.flags", aka the package name for generated AppSearch flag classes in
+ // the framework, plus an additional trailing '.'.
+ private static final String FLAG_PREFIX =
+ "com.android.appsearch.flags.";
+
+ // The full string values for flags defined in the framework.
+ //
+ // The values of the static variables are the names of the flag defined in the framework's
+ // aconfig files. E.g. "enable_safe_parcelable", with FLAG_PREFIX as the prefix.
+ //
+ // The name of the each static variable should be "FLAG_" + capitalized value of the flag.
+
+ /** Enable SafeParcelable related features. */
+ public static final String FLAG_ENABLE_SAFE_PARCELABLE_2 =
+ FLAG_PREFIX + "enable_safe_parcelable_2";
+
+ /** Enable the "hasProperty" function in list filter query expressions. */
+ public static final String FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION =
+ FLAG_PREFIX + "enable_list_filter_has_property_function";
+
+ /** Enable the "tokenize" function in list filter query expressions. */
+ public static final String FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION =
+ FLAG_PREFIX + "enable_list_filter_tokenize_function";
+
+
+ /** Enable Schema Type Grouping related features. */
+ public static final String FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA =
+ FLAG_PREFIX + "enable_grouping_type_per_schema";
+
+ /** Enable GenericDocument to take another GenericDocument to copy construct. */
+ public static final String FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR =
+ FLAG_PREFIX + "enable_generic_document_copy_constructor";
+
+ /**
+ * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and
+ * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties}.
+ */
+ public static final String FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES =
+ FLAG_PREFIX + "enable_search_spec_filter_properties";
+ /**
+ * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} method.
+ */
+ public static final String FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG =
+ FLAG_PREFIX + "enable_search_spec_set_search_source_log_tag";
+
+ /** Enable addTakenActions API in PutDocumentsRequest. */
+ public static final String FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS =
+ FLAG_PREFIX + "enable_put_documents_request_add_taken_actions";
+
+ /** Enable setPubliclyVisibleSchema in SetSchemaRequest. */
+ public static final String FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA = FLAG_PREFIX
+ + "enable_set_publicly_visible_schema";
+
+ /**
+ * Enable {@link androidx.appsearch.app.GenericDocument.Builder} to use previously hidden
+ * methods.
+ */
+ public static final String FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS = FLAG_PREFIX
+ + "enable_generic_document_builder_hidden_methods";
+
+ public static final String FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS = FLAG_PREFIX
+ + "enable_set_schema_visible_to_configs";
+
+ /** Enable {@link androidx.appsearch.app.EnterpriseGlobalSearchSession}. */
+ public static final String FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION =
+ FLAG_PREFIX + "enable_enterprise_global_search_session";
+
+ /**
+ * Enables {@link android.app.appsearch.functions.AppFunctionManager} and app functions related
+ * stuff.
+ */
+ public static final String FLAG_ENABLE_APP_FUNCTIONS = FLAG_PREFIX + "enable_app_functions";
+
+ /**
+ * Enable {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and
+ * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} which were previously
+ * hidden.
+ */
+ public static final String FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED =
+ FLAG_PREFIX + "enable_result_denied_and_result_rate_limited";
+
+ /**
+ * Enables {@link AppSearchSchema#getParentTypes()},
+ * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of
+ * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}.
+ */
+ public static final String FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES =
+ FLAG_PREFIX + "enable_get_parent_types_and_indexable_nested_properties";
+
+ /** Enables embedding search related APIs. */
+ public static final String FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG =
+ FLAG_PREFIX + "enable_schema_embedding_property_config";
+
+ /** Enables informational ranking expressions. */
+ public static final String FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS =
+ FLAG_PREFIX + "enable_informational_ranking_expressions";
+
+ // Whether the features should be enabled.
+ //
+ // In Jetpack, those should always return true.
+
+ /** Whether SafeParcelable should be enabled. */
+ public static boolean enableSafeParcelable() {
+ return true;
+ }
+
+ /** Whether the "hasProperty" function in list filter query expressions should be enabled. */
+ public static boolean enableListFilterHasPropertyFunction() {
+ return true;
+ }
+
+ /** Whether Schema Type Grouping should be enabled. */
+ public static boolean enableGroupingTypePerSchema() {
+ return true;
+ }
+
+ /** Whether Generic Document Copy Constructing should be enabled. */
+ public static boolean enableGenericDocumentCopyConstructor() {
+ return true;
+ }
+
+ /**
+ * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and
+ * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties} should be
+ * enabled.
+ */
+ public static boolean enableSearchSpecFilterProperties() {
+ return true;
+ }
+
+ /**
+ * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} should
+ * be enabled.
+ */
+ public static boolean enableSearchSpecSetSearchSourceLogTag() {
+ return true;
+ }
+
+ /** Whether addTakenActions API in PutDocumentsRequest should be enabled. */
+ public static boolean enablePutDocumentsRequestAddTakenActions() {
+ return true;
+ }
+
+ /** Whether setPubliclyVisibleSchema in SetSchemaRequest.Builder should be enabled. */
+ public static boolean enableSetPubliclyVisibleSchema() {
+ return true;
+ }
+
+ /**
+ * Whether {@link androidx.appsearch.app.GenericDocument.Builder#setNamespace(String)},
+ * {@link androidx.appsearch.app.GenericDocument.Builder#setId(String)},
+ * {@link androidx.appsearch.app.GenericDocument.Builder#setSchemaType(String)}, and
+ * {@link androidx.appsearch.app.GenericDocument.Builder#clearProperty(String)}
+ * should be enabled.
+ */
+ public static boolean enableGenericDocumentBuilderHiddenMethods() {
+ return true;
+ }
+
+ /**
+ * Whether
+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder #setSchemaTypeVisibilityForConfigs}
+ * should be enabled.
+ */
+ public static boolean enableSetSchemaVisibleToConfigs() {
+ return true;
+ }
+
+ /** Whether {@link androidx.appsearch.app.EnterpriseGlobalSearchSession} should be enabled. */
+ public static boolean enableEnterpriseGlobalSearchSession() {
+ return true;
+ }
+
+ /**
+ * Whether {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and
+ * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} should be enabled.
+ */
+ public static boolean enableResultDeniedAndResultRateLimited() {
+ return true;
+ }
+
+ /**
+ * Whether {@link AppSearchSchema#getParentTypes()},
+ * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of
+ * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}
+ * should be enabled.
+ */
+ public static boolean enableGetParentTypesAndIndexableNestedProperties() {
+ return true;
+ }
+
+ /** Whether embedding search related APIs should be enabled. */
+ public static boolean enableSchemaEmbeddingPropertyConfig() {
+ return true;
+ }
+
+ /** Whether the "tokenize" function in list filter query expressions should be enabled. */
+ public static boolean enableListFilterTokenizeFunction() {
+ return true;
+ }
+
+ /** Whether informational ranking expressions should be enabled. */
+ public static boolean enableInformationalRankingExpressions() {
+ return true;
+ }
+}
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 903929b..baae8d1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
@@ -17,7 +17,8 @@
package androidx.appsearch.observer;
import android.annotation.SuppressLint;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,6 +28,11 @@
import androidx.appsearch.app.DocumentClassFactory;
import androidx.appsearch.app.DocumentClassFactoryRegistry;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.ObserverSpecCreator;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -41,31 +47,27 @@
* 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";
[email protected](creator = "ObserverSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class ObserverSpec extends AbstractSafeParcelable {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @NonNull
+ public static final Parcelable.Creator<ObserverSpec> CREATOR =
+ new ObserverSpecCreator();
- private final Bundle mBundle;
+ @Field(id = 1)
+ final List<String> mFilterSchemas;
/** Populated on first use */
- @Nullable
- private volatile Set<String> mFilterSchemas;
+ @Nullable private volatile Set<String> mFilterSchemasCached;
/** @exportToFramework:hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public ObserverSpec(@NonNull Bundle bundle) {
- Preconditions.checkNotNull(bundle);
- mBundle = bundle;
- }
-
- /**
- * Returns the {@link Bundle} backing this spec.
- *
- * @exportToFramework:hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Bundle getBundle() {
- return mBundle;
+ @Constructor
+ public ObserverSpec(
+ @Param(id = 1) @NonNull List<String> filterSchemas) {
+ mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
}
/**
@@ -75,15 +77,14 @@
*/
@NonNull
public Set<String> getFilterSchemas() {
- if (mFilterSchemas == null) {
- List<String> schemas = mBundle.getStringArrayList(FILTER_SCHEMA_FIELD);
- if (schemas == null) {
- mFilterSchemas = Collections.emptySet();
+ if (mFilterSchemasCached == null) {
+ if (mFilterSchemas == null) {
+ mFilterSchemasCached = Collections.emptySet();
} else {
- mFilterSchemas = Collections.unmodifiableSet(new ArraySet<>(schemas));
+ mFilterSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mFilterSchemas));
}
}
- return mFilterSchemas;
+ return mFilterSchemasCached;
}
/** Builder for {@link ObserverSpec} instances. */
@@ -134,7 +135,7 @@
@SuppressLint("MissingGetterMatchingBuilder")
@CanIgnoreReturnValue
@NonNull
- public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+ public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
@@ -155,12 +156,13 @@
@CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(
- @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+ @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+ throws AppSearchException {
Preconditions.checkNotNull(documentClasses);
resetIfBuilt();
List<String> schemas = new ArrayList<>(documentClasses.size());
DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
- for (Class<?> documentClass : documentClasses) {
+ for (java.lang.Class<?> documentClass : documentClasses) {
DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
schemas.add(factory.getSchemaName());
}
@@ -172,10 +174,8 @@
/** Constructs a new {@link ObserverSpec} from the contents of this builder. */
@NonNull
public ObserverSpec build() {
- Bundle bundle = new Bundle();
- bundle.putStringArrayList(FILTER_SCHEMA_FIELD, mFilterSchemas);
mBuilt = true;
- return new ObserverSpec(bundle);
+ return new ObserverSpec(mFilterSchemas);
}
private void resetIfBuilt() {
@@ -185,4 +185,11 @@
}
}
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ ObserverSpecCreator.writeToParcel(this, dest, flags);
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
index a677ff8..bdc4bb5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
@@ -211,4 +211,10 @@
*/
public void writeToParcel(@NonNull Parcel dest, int flags) {
}
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public final int describeContents() {
+ return 0;
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
index d921290..1d407fa 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
@@ -16,20 +16,22 @@
package androidx.appsearch.safeparcel;
+import android.annotation.SuppressLint;
import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator;
import androidx.collection.ArrayMap;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -41,9 +43,11 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
-public final class GenericDocumentParcel extends AbstractSafeParcelable {
+// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
+@SuppressLint("BanParcelableUsage")
+public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
@NonNull
- public static final GenericDocumentParcelCreator CREATOR =
+ public static final Parcelable.Creator<GenericDocumentParcel> CREATOR =
new GenericDocumentParcelCreator();
/** The default score of document. */
@@ -83,7 +87,15 @@
*/
@Field(id = 7, getter = "getProperties")
@NonNull
- private final PropertyParcel[] mProperties;
+ private final List<PropertyParcel> mProperties;
+
+ /**
+ * Contains all parent properties for this {@link GenericDocument} in a list.
+ *
+ */
+ @Field(id = 8, getter = "getParentTypes")
+ @Nullable
+ private final List<String> mParentTypes;
/**
* Contains all properties in {@link GenericDocument} to support getting properties via name
@@ -110,9 +122,10 @@
@Param(id = 4) long creationTimestampMillis,
@Param(id = 5) long ttlMillis,
@Param(id = 6) int score,
- @Param(id = 7) @NonNull PropertyParcel[] properties) {
+ @Param(id = 7) @NonNull List<PropertyParcel> properties,
+ @Param(id = 8) @Nullable List<String> parentTypes) {
this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
- properties, createPropertyMapFromPropertyArray(properties));
+ properties, createPropertyMapFromPropertyArray(properties), parentTypes);
}
/**
@@ -128,8 +141,9 @@
long creationTimestampMillis,
long ttlMillis,
int score,
- @NonNull PropertyParcel[] properties,
- @NonNull Map<String, PropertyParcel> propertyMap) {
+ @NonNull List<PropertyParcel> properties,
+ @NonNull Map<String, PropertyParcel> propertyMap,
+ @Nullable List<String> parentTypes) {
mNamespace = Objects.requireNonNull(namespace);
mId = Objects.requireNonNull(id);
mSchemaType = Objects.requireNonNull(schemaType);
@@ -138,14 +152,23 @@
mScore = score;
mProperties = Objects.requireNonNull(properties);
mPropertyMap = Objects.requireNonNull(propertyMap);
+ mParentTypes = parentTypes;
+ }
+
+ /** Returns the {@link GenericDocumentParcel} object from the given {@link GenericDocument}. */
+ @NonNull
+ public static GenericDocumentParcel fromGenericDocument(
+ @NonNull GenericDocument genericDocument) {
+ Objects.requireNonNull(genericDocument);
+ return genericDocument.getDocumentParcel();
}
private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
- @NonNull PropertyParcel[] properties) {
+ @NonNull List<PropertyParcel> properties) {
Objects.requireNonNull(properties);
- Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.length);
- for (int i = 0; i < properties.length; ++i) {
- PropertyParcel property = properties[i];
+ Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.size());
+ for (int i = 0; i < properties.size(); ++i) {
+ PropertyParcel property = properties.get(i);
propertyMap.put(property.getPropertyName(), property);
}
return propertyMap;
@@ -193,7 +216,7 @@
/** Returns all the properties the document has. */
@NonNull
- public PropertyParcel[] getProperties() {
+ public List<PropertyParcel> getProperties() {
return mProperties;
}
@@ -203,6 +226,12 @@
return mPropertyMap;
}
+ /** Returns the list of parent types for the {@link GenericDocument}. */
+ @Nullable
+ public List<String> getParentTypes() {
+ return mParentTypes;
+ }
+
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
@@ -218,8 +247,9 @@
&& mTtlMillis == otherDocument.mTtlMillis
&& mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
&& mScore == otherDocument.mScore
- && Arrays.equals(mProperties, otherDocument.mProperties)
- && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);
+ && Objects.equals(mProperties, otherDocument.mProperties)
+ && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)
+ && Objects.equals(mParentTypes, otherDocument.mParentTypes);
}
@Override
@@ -232,8 +262,9 @@
mTtlMillis,
mScore,
mCreationTimestampMillis,
- Arrays.hashCode(mProperties),
- mPropertyMap.hashCode());
+ Objects.hashCode(mProperties),
+ Objects.hashCode(mPropertyMap),
+ Objects.hashCode(mParentTypes));
}
return mHashCode;
}
@@ -252,10 +283,10 @@
private long mTtlMillis;
private int mScore;
private Map<String, PropertyParcel> mPropertyMap;
- private boolean mBuilt = false;
+ @Nullable private List<String> mParentTypes;
/**
- * Creates a new {@link GenericDocument.Builder}.
+ * Creates a new {@link GenericDocumentParcel.Builder}.
*
* <p>Document IDs are unique within a namespace.
*
@@ -275,7 +306,6 @@
* Creates a new {@link GenericDocumentParcel.Builder} from the given
* {@link GenericDocumentParcel}.
*/
- @VisibleForTesting
public Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
Objects.requireNonNull(documentSafeParcel);
@@ -292,6 +322,10 @@
for (PropertyParcel value : propertyMap.values()) {
mPropertyMap.put(value.getPropertyName(), value);
}
+
+ // We don't need to create a shallow copy here, as in the setter for ParentTypes we
+ // will create a new list anyway.
+ mParentTypes = documentSafeParcel.mParentTypes;
}
/**
@@ -306,7 +340,6 @@
@NonNull
public Builder setNamespace(@NonNull String namespace) {
Objects.requireNonNull(namespace);
- resetIfBuilt();
mNamespace = namespace;
return this;
}
@@ -321,7 +354,6 @@
@NonNull
public Builder setId(@NonNull String id) {
Objects.requireNonNull(id);
- resetIfBuilt();
mId = id;
return this;
}
@@ -336,7 +368,6 @@
@NonNull
public Builder setSchemaType(@NonNull String schemaType) {
Objects.requireNonNull(schemaType);
- resetIfBuilt();
mSchemaType = schemaType;
return this;
}
@@ -345,7 +376,6 @@
@CanIgnoreReturnValue
@NonNull
public Builder setScore(int score) {
- resetIfBuilt();
mScore = score;
return this;
}
@@ -364,7 +394,6 @@
@NonNull
public Builder setCreationTimestampMillis(
/*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
- resetIfBuilt();
mCreationTimestampMillis = creationTimestampMillis;
return this;
}
@@ -388,12 +417,24 @@
if (ttlMillis < 0) {
throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
}
- resetIfBuilt();
mTtlMillis = ttlMillis;
return this;
}
/**
+ * Sets the list of parent types of the {@link GenericDocument}'s type.
+ *
+ * <p>Child types must appear before parent types in the list.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setParentTypes(@NonNull List<String> parentTypes) {
+ Objects.requireNonNull(parentTypes);
+ mParentTypes = new ArrayList<>(parentTypes);
+ return this;
+ }
+
+ /**
* Clears the value for the property with the given name.
*
* <p>Note that this method does not support property paths.
@@ -404,40 +445,43 @@
@NonNull
public Builder clearProperty(@NonNull String name) {
Objects.requireNonNull(name);
- resetIfBuilt();
mPropertyMap.remove(name);
return this;
}
/** puts an array of {@link String} in property map. */
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
throws IllegalArgumentException {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setStringValues(values).build());
return this;
}
/** puts an array of boolean in property map. */
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setBooleanValues(values).build());
return this;
}
/** puts an array of double in property map. */
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setDoubleValues(values).build());
return this;
}
/** puts an array of long in property map. */
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setLongValues(values).build());
return this;
}
@@ -445,26 +489,47 @@
/**
* Converts and saves a byte[][] into {@link #mProperties}.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setBytesValues(values).build());
return this;
}
/** puts an array of {@link GenericDocumentParcel} in property map. */
+ @CanIgnoreReturnValue
@NonNull
public Builder putInPropertyMap(@NonNull String name,
@NonNull GenericDocumentParcel[] values) {
- mPropertyMap.put(name,
+ putInPropertyMap(name,
new PropertyParcel.Builder(name).setDocumentValues(values).build());
return this;
}
+ /** puts an array of {@link EmbeddingVector} in property map. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name,
+ @NonNull EmbeddingVector[] values) {
+ putInPropertyMap(name,
+ new PropertyParcel.Builder(name).setEmbeddingValues(values).build());
+ return this;
+ }
+
+ /** Directly puts a {@link PropertyParcel} in property map. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder putInPropertyMap(@NonNull String name,
+ @NonNull PropertyParcel value) {
+ Objects.requireNonNull(value);
+ mPropertyMap.put(name, value);
+ return this;
+ }
+
/** Builds the {@link GenericDocument} object. */
@NonNull
public GenericDocumentParcel build() {
- mBuilt = true;
// Set current timestamp for creation timestamp by default.
if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
mCreationTimestampMillis = System.currentTimeMillis();
@@ -476,19 +541,8 @@
mCreationTimestampMillis,
mTtlMillis,
mScore,
- mPropertyMap.values().toArray(new PropertyParcel[0]));
- }
-
- void resetIfBuilt() {
- if (mBuilt) {
- Map<String, PropertyParcel> propertyMap = mPropertyMap;
- mPropertyMap = new ArrayMap<>(propertyMap.size());
- for (PropertyParcel value : propertyMap.values()) {
- // PropertyParcel is not deep copied since it is not mutable.
- mPropertyMap.put(value.getPropertyName(), value);
- }
- mBuilt = false;
- }
+ new ArrayList<>(mPropertyMap.values()),
+ mParentTypes);
}
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
new file mode 100644
index 0000000..98aee5b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2023 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.safeparcel;
+
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implemented creator for {@link GenericDocumentParcel}.
+ *
+ * <p>In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},
+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class GenericDocumentParcelCreator implements
+ Parcelable.Creator<GenericDocumentParcel> {
+ private static final String PROPERTIES_FIELD = "properties";
+ private static final String SCHEMA_TYPE_FIELD = "schemaType";
+ private static final String ID_FIELD = "id";
+ private static final String SCORE_FIELD = "score";
+ private static final String TTL_MILLIS_FIELD = "ttlMillis";
+ private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
+ private static final String NAMESPACE_FIELD = "namespace";
+ private static final String PARENT_TYPES_FIELD = "parentTypes";
+
+ /** Creates a {@link GenericDocumentParcel} from a {@link Bundle}. */
+ @NonNull
+ private static GenericDocumentParcel createGenericDocumentParcelFromBundle(
+ @NonNull Bundle genericDocumentParcelBundle) {
+ // Get namespace, id, and schema type
+ String namespace = genericDocumentParcelBundle.getString(NAMESPACE_FIELD);
+ String id = genericDocumentParcelBundle.getString(ID_FIELD);
+ String schemaType = genericDocumentParcelBundle.getString(SCHEMA_TYPE_FIELD);
+
+ // Those three can NOT be null.
+ if (namespace == null || id == null || schemaType == null) {
+ throw new IllegalArgumentException("GenericDocumentParcel bundle doesn't have "
+ + "namespace, id, or schemaType.");
+ }
+
+ GenericDocumentParcel.Builder builder = new GenericDocumentParcel.Builder(namespace,
+ id, schemaType);
+ List<String> parentTypes =
+ genericDocumentParcelBundle.getStringArrayList(PARENT_TYPES_FIELD);
+ if (parentTypes != null) {
+ builder.setParentTypes(parentTypes);
+ }
+ builder.setScore(genericDocumentParcelBundle.getInt(SCORE_FIELD));
+ builder.setCreationTimestampMillis(
+ genericDocumentParcelBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD));
+ builder.setTtlMillis(genericDocumentParcelBundle.getLong(TTL_MILLIS_FIELD));
+
+ // properties
+ Bundle propertyBundle = genericDocumentParcelBundle.getBundle(PROPERTIES_FIELD);
+ if (propertyBundle != null) {
+ for (String propertyName : propertyBundle.keySet()) {
+ // SuppressWarnings can be applied on a local variable, but not any
+ // single line of code.
+ @SuppressWarnings("deprecation")
+ PropertyParcel propertyParcel = propertyBundle.getParcelable(propertyName);
+ builder.putInPropertyMap(propertyName, propertyParcel);
+ }
+ }
+
+ return builder.build();
+ }
+
+ /** Creates a {@link Bundle} from a {@link GenericDocumentParcel}. */
+ @NonNull
+ private static Bundle createBundleFromGenericDocumentParcel(
+ @NonNull GenericDocumentParcel genericDocumentParcel) {
+ Bundle genericDocumentParcelBundle = new Bundle();
+
+ // Common fields
+ genericDocumentParcelBundle.putString(NAMESPACE_FIELD,
+ genericDocumentParcel.getNamespace());
+ genericDocumentParcelBundle.putString(ID_FIELD, genericDocumentParcel.getId());
+ genericDocumentParcelBundle.putString(SCHEMA_TYPE_FIELD,
+ genericDocumentParcel.getSchemaType());
+ genericDocumentParcelBundle.putStringArrayList(PARENT_TYPES_FIELD,
+ (ArrayList<String>) genericDocumentParcel.getParentTypes());
+ genericDocumentParcelBundle.putInt(SCORE_FIELD, genericDocumentParcel.getScore());
+ genericDocumentParcelBundle.putLong(CREATION_TIMESTAMP_MILLIS_FIELD,
+ genericDocumentParcel.getCreationTimestampMillis());
+ genericDocumentParcelBundle.putLong(TTL_MILLIS_FIELD,
+ genericDocumentParcel.getTtlMillis());
+
+ // Properties
+ Bundle properties = new Bundle();
+ List<PropertyParcel> propertyParcels = genericDocumentParcel.getProperties();
+ for (int i = 0; i < propertyParcels.size(); ++i) {
+ PropertyParcel propertyParcel = propertyParcels.get(i);
+ properties.putParcelable(propertyParcel.getPropertyName(), propertyParcel);
+ }
+ genericDocumentParcelBundle.putBundle(PROPERTIES_FIELD, properties);
+
+ return genericDocumentParcelBundle;
+ }
+
+ @Nullable
+ @Override
+ public GenericDocumentParcel createFromParcel(Parcel in) {
+ Bundle bundle = in.readBundle(getClass().getClassLoader());
+ return createGenericDocumentParcelFromBundle(bundle);
+ }
+
+ @Override
+ public GenericDocumentParcel[] newArray(int size) {
+ return new GenericDocumentParcel[size];
+ }
+
+ /** Writes a {@link GenericDocumentParcel} to a {@link Parcel}. */
+ public static void writeToParcel(@NonNull GenericDocumentParcel genericDocumentParcel,
+ @NonNull android.os.Parcel parcel, int flags) {
+ parcel.writeBundle(createBundleFromGenericDocumentParcel(genericDocumentParcel));
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
new file mode 100644
index 0000000..f405f48
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 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.safeparcel;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Holds data for a {@link PackageIdentifier}.
+ *
+ * TODO(b/275592563): This class is currently used in GetSchemaResponse as a bundle, and
+ * therefore needs to implement Parcelable directly. Reassess if this is still needed once
+ * VisibilityConfig becomes available, and if not we should switch to a SafeParcelable
+ * implementation instead.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
[email protected](creator = "PackageIdentifierParcelCreator")
+@SuppressLint("BanParcelableUsage")
+public final class PackageIdentifierParcel extends AbstractSafeParcelable implements Parcelable {
+ @NonNull
+ public static final Parcelable.Creator<PackageIdentifierParcel> CREATOR =
+ new PackageIdentifierParcelCreator();
+
+ @Field(id = 1, getter = "getPackageName")
+ private final String mPackageName;
+ @Field(id = 2, getter = "getSha256Certificate")
+ private final byte[] mSha256Certificate;
+
+ /**
+ * Creates a unique identifier for a package.
+ *
+ * @see PackageIdentifier
+ */
+ @Constructor
+ public PackageIdentifierParcel(@Param(id = 1) @NonNull String packageName,
+ @Param(id = 2) @NonNull byte[] sha256Certificate) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @NonNull
+ public byte[] getSha256Certificate() {
+ return mSha256Certificate;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof PackageIdentifierParcel)) {
+ return false;
+ }
+ final PackageIdentifierParcel other = (PackageIdentifierParcel) obj;
+ return mPackageName.equals(other.mPackageName)
+ && Arrays.equals(mSha256Certificate, other.mSha256Certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, Arrays.hashCode(mSha256Certificate));
+ }
+
+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ PackageIdentifierParcelCreator.writeToParcel(this, dest, flags);
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
new file mode 100644
index 0000000..16fe177
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 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.safeparcel;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * An implemented creator for {@link PackageIdentifierParcel}.
+ *
+ * <p>In Jetpack, {@link androidx.appsearch.app.PackageIdentifier} is serialized in a bundle for
+ * {@link androidx.appsearch.app.GetSchemaResponse}, and therefore needs to implement a real
+ * {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PackageIdentifierParcelCreator implements Parcelable.Creator<PackageIdentifierParcel> {
+ private static final String PACKAGE_NAME_FIELD = "packageName";
+ private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
+
+ public PackageIdentifierParcelCreator() {
+ }
+
+ /**
+ * Creates a {@link PackageIdentifierParcel} from a {@link Bundle}
+ */
+ @NonNull
+ private static PackageIdentifierParcel createPackageIdentifierFromBundle(
+ @NonNull Bundle packageIdentifierBundle) {
+ Objects.requireNonNull(packageIdentifierBundle);
+ String packageName =
+ Preconditions.checkNotNull(packageIdentifierBundle.getString(PACKAGE_NAME_FIELD));
+ byte[] sha256Certificate =
+ Preconditions.checkNotNull(
+ packageIdentifierBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
+
+ return new PackageIdentifierParcel(packageName, sha256Certificate);
+ }
+
+ /** Creates a {@link Bundle} from a {@link PackageIdentifierParcel}. */
+ @NonNull
+ private static Bundle createBundleFromPackageIdentifier(
+ @NonNull PackageIdentifierParcel packageIdentifierParcel) {
+ Objects.requireNonNull(packageIdentifierParcel);
+ Bundle packageIdentifierBundle = new Bundle();
+ packageIdentifierBundle.putString(PACKAGE_NAME_FIELD,
+ packageIdentifierParcel.getPackageName());
+ packageIdentifierBundle.putByteArray(SHA256_CERTIFICATE_FIELD,
+ packageIdentifierParcel.getSha256Certificate());
+
+ return packageIdentifierBundle;
+ }
+
+ @NonNull
+ @Override
+ public PackageIdentifierParcel createFromParcel(Parcel parcel) {
+ Bundle bundle = Preconditions.checkNotNull(parcel.readBundle(getClass().getClassLoader()));
+ return createPackageIdentifierFromBundle(bundle);
+ }
+
+ @NonNull
+ @Override
+ public PackageIdentifierParcel[] newArray(int size) {
+ return new PackageIdentifierParcel[size];
+ }
+
+ /** Writes a {@link PackageIdentifierParcel} to a {@link Parcel}. */
+ public static void writeToParcel(@NonNull PackageIdentifierParcel packageIdentifierParcel,
+ @NonNull android.os.Parcel parcel, int flags) {
+ parcel.writeBundle(createBundleFromPackageIdentifier(packageIdentifierParcel));
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
index 61029bd..96c53ae 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
@@ -17,6 +17,7 @@
package androidx.appsearch.safeparcel;
import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,10 +28,12 @@
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JoinableValueType;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TokenizerType;
import androidx.appsearch.safeparcel.stub.StubCreators.DocumentIndexingConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingIndexingConfigParcelCreator;
import androidx.appsearch.safeparcel.stub.StubCreators.IntegerIndexingConfigParcelCreator;
import androidx.appsearch.safeparcel.stub.StubCreators.JoinableConfigParcelCreator;
import androidx.appsearch.safeparcel.stub.StubCreators.PropertyConfigParcelCreator;
import androidx.appsearch.safeparcel.stub.StubCreators.StringIndexingConfigParcelCreator;
+import androidx.core.util.ObjectsCompat;
import java.util.List;
import java.util.Objects;
@@ -49,7 +52,8 @@
@SafeParcelable.Class(creator = "PropertyConfigParcelCreator")
public final class PropertyConfigParcel extends AbstractSafeParcelable {
@NonNull
- public static final PropertyConfigParcelCreator CREATOR = new PropertyConfigParcelCreator();
+ public static final Parcelable.Creator<PropertyConfigParcel> CREATOR =
+ new PropertyConfigParcelCreator();
@Field(id = 1, getter = "getName")
private final String mName;
@@ -63,23 +67,32 @@
private final int mCardinality;
@Field(id = 4, getter = "getSchemaType")
- private final String mSchemaType;
+ @Nullable private final String mSchemaType;
@Field(id = 5, getter = "getStringIndexingConfigParcel")
- private final StringIndexingConfigParcel mStringIndexingConfigParcel;
+ @Nullable private final StringIndexingConfigParcel mStringIndexingConfigParcel;
@Field(id = 6, getter = "getDocumentIndexingConfigParcel")
- private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
+ @Nullable private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
@Field(id = 7, getter = "getIntegerIndexingConfigParcel")
- private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
+ @Nullable private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
@Field(id = 8, getter = "getJoinableConfigParcel")
- private final JoinableConfigParcel mJoinableConfigParcel;
+ @Nullable private final JoinableConfigParcel mJoinableConfigParcel;
+
+ @Field(id = 9, getter = "getDescription")
+ private final String mDescription;
+
+ @Field(id = 10, getter = "getEmbeddingIndexingConfigParcel")
+ private final EmbeddingIndexingConfigParcel mEmbeddingIndexingConfigParcel;
+
+ @Nullable
+ private Integer mHashCode;
/** Constructor for {@link PropertyConfigParcel}. */
@Constructor
- public PropertyConfigParcel(
+ PropertyConfigParcel(
@Param(id = 1) @NonNull String name,
@Param(id = 2) @DataType int dataType,
@Param(id = 3) @Cardinality int cardinality,
@@ -87,7 +100,9 @@
@Param(id = 5) @Nullable StringIndexingConfigParcel stringIndexingConfigParcel,
@Param(id = 6) @Nullable DocumentIndexingConfigParcel documentIndexingConfigParcel,
@Param(id = 7) @Nullable IntegerIndexingConfigParcel integerIndexingConfigParcel,
- @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel) {
+ @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel,
+ @Param(id = 9) @NonNull String description,
+ @Param(id = 10) @Nullable EmbeddingIndexingConfigParcel embeddingIndexingConfigParcel) {
mName = Objects.requireNonNull(name);
mDataType = dataType;
mCardinality = cardinality;
@@ -96,6 +111,147 @@
mDocumentIndexingConfigParcel = documentIndexingConfigParcel;
mIntegerIndexingConfigParcel = integerIndexingConfigParcel;
mJoinableConfigParcel = joinableConfigParcel;
+ mDescription = Objects.requireNonNull(description);
+ mEmbeddingIndexingConfigParcel = embeddingIndexingConfigParcel;
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for String. */
+ @NonNull
+ public static PropertyConfigParcel createForString(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality,
+ @NonNull StringIndexingConfigParcel stringIndexingConfigParcel,
+ @NonNull JoinableConfigParcel joinableConfigParcel) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_STRING,
+ cardinality,
+ /*schemaType=*/ null,
+ Objects.requireNonNull(stringIndexingConfigParcel),
+ /*documentIndexingConfigParcel=*/ null,
+ /*integerIndexingConfigParcel=*/ null,
+ Objects.requireNonNull(joinableConfigParcel),
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Long. */
+ @NonNull
+ public static PropertyConfigParcel createForLong(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality,
+ @AppSearchSchema.LongPropertyConfig.IndexingType int indexingType) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_LONG,
+ cardinality,
+ /*schemaType=*/ null,
+ /*stringIndexingConfigParcel=*/ null,
+ /*documentIndexingConfigParcel=*/ null,
+ new IntegerIndexingConfigParcel(indexingType),
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Double. */
+ @NonNull
+ public static PropertyConfigParcel createForDouble(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE,
+ cardinality,
+ /*schemaType=*/ null,
+ /*stringIndexingConfigParcel=*/ null,
+ /*documentIndexingConfigParcel=*/ null,
+ /*integerIndexingConfigParcel=*/ null,
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Boolean. */
+ @NonNull
+ public static PropertyConfigParcel createForBoolean(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN,
+ cardinality,
+ /*schemaType=*/ null,
+ /*stringIndexingConfigParcel=*/ null,
+ /*documentIndexingConfigParcel=*/ null,
+ /*integerIndexingConfigParcel=*/ null,
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Bytes. */
+ @NonNull
+ public static PropertyConfigParcel createForBytes(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES,
+ cardinality,
+ /*schemaType=*/ null,
+ /*stringIndexingConfigParcel=*/ null,
+ /*documentIndexingConfigParcel=*/ null,
+ /*integerIndexingConfigParcel=*/ null,
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Document. */
+ @NonNull
+ public static PropertyConfigParcel createForDocument(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality,
+ @NonNull String schemaType,
+ @NonNull DocumentIndexingConfigParcel documentIndexingConfigParcel) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT,
+ cardinality,
+ Objects.requireNonNull(schemaType),
+ /*stringIndexingConfigParcel=*/ null,
+ Objects.requireNonNull(documentIndexingConfigParcel),
+ /*integerIndexingConfigParcel=*/ null,
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ /*embeddingIndexingConfigParcel=*/ null);
+ }
+
+ /** Creates a {@link PropertyConfigParcel} for Embedding. */
+ @NonNull
+ public static PropertyConfigParcel createForEmbedding(
+ @NonNull String propertyName,
+ @NonNull String description,
+ @Cardinality int cardinality,
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+ return new PropertyConfigParcel(
+ Objects.requireNonNull(propertyName),
+ AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING,
+ cardinality,
+ /*schemaType=*/ null,
+ /*stringIndexingConfigParcel=*/ null,
+ /*documentIndexingConfigParcel=*/ null,
+ /*integerIndexingConfigParcel=*/ null,
+ /*joinableConfigParcel=*/ null,
+ Objects.requireNonNull(description),
+ new EmbeddingIndexingConfigParcel(indexingType));
}
/** Gets name for the property. */
@@ -104,6 +260,12 @@
return mName;
}
+ /** Gets description for the property. */
+ @NonNull
+ public String getDescription() {
+ return mDescription;
+ }
+
/** Gets data type for the property. */
@DataType
public int getDataType() {
@@ -146,11 +308,84 @@
return mJoinableConfigParcel;
}
+ /** Gets the {@link EmbeddingIndexingConfigParcel}. */
+ @Nullable
+ public EmbeddingIndexingConfigParcel getEmbeddingIndexingConfigParcel() {
+ return mEmbeddingIndexingConfigParcel;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PropertyConfigParcel)) {
+ return false;
+ }
+ PropertyConfigParcel otherProperty = (PropertyConfigParcel) other;
+ return ObjectsCompat.equals(mName, otherProperty.mName)
+ && Objects.equals(mDescription, otherProperty.mDescription)
+ && ObjectsCompat.equals(mDataType, otherProperty.mDataType)
+ && ObjectsCompat.equals(mCardinality, otherProperty.mCardinality)
+ && ObjectsCompat.equals(mSchemaType, otherProperty.mSchemaType)
+ && ObjectsCompat.equals(
+ mStringIndexingConfigParcel, otherProperty.mStringIndexingConfigParcel)
+ && ObjectsCompat.equals(
+ mDocumentIndexingConfigParcel, otherProperty.mDocumentIndexingConfigParcel)
+ && ObjectsCompat.equals(
+ mIntegerIndexingConfigParcel, otherProperty.mIntegerIndexingConfigParcel)
+ && ObjectsCompat.equals(
+ mJoinableConfigParcel, otherProperty.mJoinableConfigParcel)
+ && ObjectsCompat.equals(
+ mEmbeddingIndexingConfigParcel, otherProperty.mEmbeddingIndexingConfigParcel);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode =
+ ObjectsCompat.hash(
+ mName,
+ mDescription,
+ mDataType,
+ mCardinality,
+ mSchemaType,
+ mStringIndexingConfigParcel,
+ mDocumentIndexingConfigParcel,
+ mIntegerIndexingConfigParcel,
+ mJoinableConfigParcel,
+ mEmbeddingIndexingConfigParcel);
+ }
+ return mHashCode;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{name: " + mName
+ + ", description: " + mDescription
+ + ", dataType: " + mDataType
+ + ", cardinality: " + mCardinality
+ + ", schemaType: " + mSchemaType
+ + ", stringIndexingConfigParcel: " + mStringIndexingConfigParcel
+ + ", documentIndexingConfigParcel: " + mDocumentIndexingConfigParcel
+ + ", integerIndexingConfigParcel: " + mIntegerIndexingConfigParcel
+ + ", joinableConfigParcel: " + mJoinableConfigParcel
+ + ", embeddingIndexingConfigParcel: " + mEmbeddingIndexingConfigParcel
+ + "}";
+ }
+
/** Class to hold join configuration for a String type. */
@SafeParcelable.Class(creator = "JoinableConfigParcelCreator")
public static class JoinableConfigParcel extends AbstractSafeParcelable {
@NonNull
- public static final JoinableConfigParcelCreator CREATOR = new JoinableConfigParcelCreator();
+ public static final Parcelable.Creator<JoinableConfigParcel> CREATOR =
+ new JoinableConfigParcelCreator();
@JoinableValueType
@Field(id = 1, getter = "getJoinableValueType")
@@ -183,13 +418,38 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
JoinableConfigParcelCreator.writeToParcel(this, dest, flags);
}
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(mJoinableValueType, mDeletionPropagation);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof JoinableConfigParcel)) {
+ return false;
+ }
+ JoinableConfigParcel otherObject = (JoinableConfigParcel) other;
+ return ObjectsCompat.equals(mJoinableValueType, otherObject.mJoinableValueType)
+ && ObjectsCompat.equals(mDeletionPropagation, otherObject.mDeletionPropagation);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{joinableValueType: " + mJoinableValueType
+ + ", deletePropagation " + mDeletionPropagation + "}";
+ }
}
/** Class to hold configuration a string type. */
@SafeParcelable.Class(creator = "StringIndexingConfigParcelCreator")
public static class StringIndexingConfigParcel extends AbstractSafeParcelable {
@NonNull
- public static final StringIndexingConfigParcelCreator CREATOR =
+ public static final Parcelable.Creator<StringIndexingConfigParcel> CREATOR =
new StringIndexingConfigParcelCreator();
@AppSearchSchema.StringPropertyConfig.IndexingType
@@ -225,13 +485,38 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
StringIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
}
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(mIndexingType, mTokenizerType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof StringIndexingConfigParcel)) {
+ return false;
+ }
+ StringIndexingConfigParcel otherObject = (StringIndexingConfigParcel) other;
+ return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType)
+ && ObjectsCompat.equals(mTokenizerType, otherObject.mTokenizerType);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{indexingType: " + mIndexingType
+ + ", tokenizerType " + mTokenizerType + "}";
+ }
}
/** Class to hold configuration for integer property type. */
@SafeParcelable.Class(creator = "IntegerIndexingConfigParcelCreator")
public static class IntegerIndexingConfigParcel extends AbstractSafeParcelable {
@NonNull
- public static final IntegerIndexingConfigParcelCreator CREATOR =
+ public static final Parcelable.Creator<IntegerIndexingConfigParcel> CREATOR =
new IntegerIndexingConfigParcelCreator();
@AppSearchSchema.LongPropertyConfig.IndexingType
@@ -255,13 +540,36 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
IntegerIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
}
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hashCode(mIndexingType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof IntegerIndexingConfigParcel)) {
+ return false;
+ }
+ IntegerIndexingConfigParcel otherObject = (IntegerIndexingConfigParcel) other;
+ return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{indexingType: " + mIndexingType + "}";
+ }
}
/** Class to hold configuration for document property type. */
@SafeParcelable.Class(creator = "DocumentIndexingConfigParcelCreator")
public static class DocumentIndexingConfigParcel extends AbstractSafeParcelable {
@NonNull
- public static final DocumentIndexingConfigParcelCreator CREATOR =
+ public static final Parcelable.Creator<DocumentIndexingConfigParcel> CREATOR =
new DocumentIndexingConfigParcelCreator();
@Field(id = 1, getter = "shouldIndexNestedProperties")
@@ -295,10 +603,86 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
DocumentIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
}
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(mIndexNestedProperties, mIndexableNestedPropertiesList);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DocumentIndexingConfigParcel)) {
+ return false;
+ }
+ DocumentIndexingConfigParcel otherObject = (DocumentIndexingConfigParcel) other;
+ return ObjectsCompat.equals(mIndexNestedProperties, otherObject.mIndexNestedProperties)
+ && ObjectsCompat.equals(mIndexableNestedPropertiesList,
+ otherObject.mIndexableNestedPropertiesList);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{indexNestedProperties: " + mIndexNestedProperties
+ + ", indexableNestedPropertiesList: " + mIndexableNestedPropertiesList
+ + "}";
+ }
}
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
+ /** Class to hold configuration for embedding property. */
+ @SafeParcelable.Class(creator = "EmbeddingIndexingConfigParcelCreator")
+ public static class EmbeddingIndexingConfigParcel extends AbstractSafeParcelable {
+ @NonNull
+ public static final Parcelable.Creator<EmbeddingIndexingConfigParcel> CREATOR =
+ new EmbeddingIndexingConfigParcelCreator();
+
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+ @Field(id = 1, getter = "getIndexingType")
+ private final int mIndexingType;
+
+ /** Constructor for {@link EmbeddingIndexingConfigParcel}. */
+ @Constructor
+ public EmbeddingIndexingConfigParcel(
+ @Param(id = 1) @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+ int indexingType) {
+ mIndexingType = indexingType;
+ }
+
+ /** Gets the indexing type for this embedding property. */
+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+ public int getIndexingType() {
+ return mIndexingType;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ EmbeddingIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hashCode(mIndexingType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof EmbeddingIndexingConfigParcel)) {
+ return false;
+ }
+ EmbeddingIndexingConfigParcel otherObject = (EmbeddingIndexingConfigParcel) other;
+ return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{indexingType: " + mIndexingType + "}";
+ }
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
index 3762adc..3e4fb3a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
@@ -17,12 +17,15 @@
package androidx.appsearch.safeparcel;
+import android.annotation.SuppressLint;
import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.EmbeddingVector;
import java.util.Arrays;
import java.util.Objects;
@@ -36,8 +39,11 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SafeParcelable.Class(creator = "PropertyParcelCreator")
-public final class PropertyParcel extends AbstractSafeParcelable {
- @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
+// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
+@SuppressLint("BanParcelableUsage")
+public final class PropertyParcel extends AbstractSafeParcelable implements Parcelable {
+ @NonNull public static final Parcelable.Creator<PropertyParcel> CREATOR =
+ new PropertyParcelCreator();
@NonNull
@Field(id = 1, getter = "getPropertyName")
@@ -67,6 +73,10 @@
@Field(id = 7, getter = "getDocumentValues")
private final GenericDocumentParcel[] mDocumentValues;
+ @Nullable
+ @Field(id = 8, getter = "getEmbeddingValues")
+ private final EmbeddingVector[] mEmbeddingValues;
+
@Nullable private Integer mHashCode;
@Constructor
@@ -77,7 +87,8 @@
@Param(id = 4) @Nullable double[] doubleValues,
@Param(id = 5) @Nullable boolean[] booleanValues,
@Param(id = 6) @Nullable byte[][] bytesValues,
- @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues) {
+ @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues,
+ @Param(id = 8) @Nullable EmbeddingVector[] embeddingValues) {
mPropertyName = Objects.requireNonNull(propertyName);
mStringValues = stringValues;
mLongValues = longValues;
@@ -85,6 +96,7 @@
mBooleanValues = booleanValues;
mBytesValues = bytesValues;
mDocumentValues = documentValues;
+ mEmbeddingValues = embeddingValues;
checkOnlyOneArrayCanBeSet();
}
@@ -130,6 +142,12 @@
return mDocumentValues;
}
+ /** Returns {@link EmbeddingVector}s in an array. */
+ @Nullable
+ public EmbeddingVector[] getEmbeddingValues() {
+ return mEmbeddingValues;
+ }
+
/**
* Returns the held values in an array for this property.
*
@@ -155,6 +173,9 @@
if (mDocumentValues != null) {
return mDocumentValues;
}
+ if (mEmbeddingValues != null) {
+ return mEmbeddingValues;
+ }
return null;
}
@@ -183,6 +204,9 @@
if (mDocumentValues != null) {
++notNullCount;
}
+ if (mEmbeddingValues != null) {
+ ++notNullCount;
+ }
if (notNullCount == 0 || notNullCount > 1) {
throw new IllegalArgumentException(
"One and only one type array can be set in PropertyParcel");
@@ -205,6 +229,8 @@
hashCode = Arrays.deepHashCode(mBytesValues);
} else if (mDocumentValues != null) {
hashCode = Arrays.hashCode(mDocumentValues);
+ } else if (mEmbeddingValues != null) {
+ hashCode = Arrays.deepHashCode(mEmbeddingValues);
}
mHashCode = Objects.hash(mPropertyName, hashCode);
}
@@ -228,7 +254,13 @@
&& Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
&& Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
&& Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
- && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues);
+ && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues)
+ && Arrays.deepEquals(mEmbeddingValues, otherPropertyParcel.mEmbeddingValues);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ PropertyParcelCreator.writeToParcel(this, dest, flags);
}
/** Builder for {@link PropertyParcel}. */
@@ -240,12 +272,14 @@
private boolean[] mBooleanValues;
private byte[][] mBytesValues;
private GenericDocumentParcel[] mDocumentValues;
+ private EmbeddingVector[] mEmbeddingValues;
public Builder(@NonNull String propertyName) {
mPropertyName = Objects.requireNonNull(propertyName);
}
/** Sets String values. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStringValues(@NonNull String[] stringValues) {
mStringValues = Objects.requireNonNull(stringValues);
@@ -253,6 +287,7 @@
}
/** Sets long values. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setLongValues(@NonNull long[] longValues) {
mLongValues = Objects.requireNonNull(longValues);
@@ -260,6 +295,7 @@
}
/** Sets double values. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDoubleValues(@NonNull double[] doubleValues) {
mDoubleValues = Objects.requireNonNull(doubleValues);
@@ -267,6 +303,7 @@
}
/** Sets boolean values. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
mBooleanValues = Objects.requireNonNull(booleanValues);
@@ -274,6 +311,7 @@
}
/** Sets a two dimension byte array. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setBytesValues(@NonNull byte[][] bytesValues) {
mBytesValues = Objects.requireNonNull(bytesValues);
@@ -281,12 +319,21 @@
}
/** Sets document values. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentValues(@NonNull GenericDocumentParcel[] documentValues) {
mDocumentValues = Objects.requireNonNull(documentValues);
return this;
}
+ /** Sets embedding values. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setEmbeddingValues(@NonNull EmbeddingVector[] embeddingValues) {
+ mEmbeddingValues = Objects.requireNonNull(embeddingValues);
+ return this;
+ }
+
/** Builds a {@link PropertyParcel}. */
@NonNull
public PropertyParcel build() {
@@ -297,12 +344,8 @@
mDoubleValues,
mBooleanValues,
mBytesValues,
- mDocumentValues);
+ mDocumentValues,
+ mEmbeddingValues);
}
}
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- PropertyParcelCreator.writeToParcel(this, dest, flags);
- }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
new file mode 100644
index 0000000..46be473
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2023 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.safeparcel;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An implemented creator for {@link PropertyParcel}.
+ *
+ * <p>In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},
+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PropertyParcelCreator implements Parcelable.Creator<PropertyParcel> {
+ private static final String PROPERTY_NAME_FIELD = "propertyName";
+ private static final String STRING_ARRAY_FIELD = "stringArray";
+ private static final String LONG_ARRAY_FIELD = "longArray";
+ private static final String DOUBLE_ARRAY_FIELD = "doubleArray";
+ private static final String BOOLEAN_ARRAY_FIELD = "booleanArray";
+ // 1d
+ private static final String BYTE_ARRAY_FIELD = "byteArray";
+ // 2d
+ private static final String BYTES_ARRAY_FIELD = "bytesArray";
+ private static final String DOC_ARRAY_FIELD = "docArray";
+ private static final String EMBEDDING_VALUE_FIELD = "embeddingValue";
+ private static final String EMBEDDING_MODEL_SIGNATURE_FIELD = "embeddingModelSignature";
+ private static final String EMBEDDING_ARRAY_FIELD = "embeddingArray";
+
+ public PropertyParcelCreator() {
+ }
+
+ /** Creates a {@link PropertyParcel} from a {@link Bundle}. */
+ @SuppressWarnings({"unchecked"})
+ @NonNull
+ private static PropertyParcel createPropertyParcelFromBundle(
+ @NonNull Bundle propertyParcelBundle) {
+ Objects.requireNonNull(propertyParcelBundle);
+ String propertyName = propertyParcelBundle.getString(PROPERTY_NAME_FIELD);
+
+ Objects.requireNonNull(propertyName);
+ PropertyParcel.Builder builder = new PropertyParcel.Builder(propertyName);
+
+ // Get the values out of the bundle.
+ String[] stringValues = propertyParcelBundle.getStringArray(STRING_ARRAY_FIELD);
+ long[] longValues = propertyParcelBundle.getLongArray(LONG_ARRAY_FIELD);
+ double[] doubleValues = propertyParcelBundle.getDoubleArray(DOUBLE_ARRAY_FIELD);
+ boolean[] booleanValues = propertyParcelBundle.getBooleanArray(BOOLEAN_ARRAY_FIELD);
+
+ List<Bundle> bytesArray;
+ // SuppressWarnings can be applied on a local variable, but not any single line of
+ // code.
+ @SuppressWarnings("deprecation")
+ List<Bundle> tmpList = propertyParcelBundle.getParcelableArrayList(BYTES_ARRAY_FIELD);
+ bytesArray = tmpList;
+
+ Parcelable[] docValues;
+ // SuppressWarnings can be applied on a local variable, but not any single line of
+ // code.
+ @SuppressWarnings("deprecation")
+ Parcelable[] tmpParcel = propertyParcelBundle.getParcelableArray(DOC_ARRAY_FIELD);
+ docValues = tmpParcel;
+
+ // SuppressWarnings can be applied on a local variable, but not any single line of
+ // code.
+ @SuppressWarnings("deprecation")
+ List<Bundle> embeddingArray = propertyParcelBundle.getParcelableArrayList(
+ EMBEDDING_ARRAY_FIELD);
+
+ // Only one of those values will be set.
+ boolean valueSet = false;
+ if (stringValues != null) {
+ builder.setStringValues(stringValues);
+ valueSet = true;
+ } else if (longValues != null) {
+ builder.setLongValues(longValues);
+ valueSet = true;
+ } else if (doubleValues != null) {
+ builder.setDoubleValues(doubleValues);
+ valueSet = true;
+ } else if (booleanValues != null) {
+ builder.setBooleanValues(booleanValues);
+ valueSet = true;
+ } else if (bytesArray != null) {
+ byte[][] bytes = new byte[bytesArray.size()][];
+ for (int i = 0; i < bytesArray.size(); i++) {
+ Bundle byteArray = bytesArray.get(i);
+ if (byteArray == null) {
+ continue;
+ }
+ byte[] innerBytes = byteArray.getByteArray(BYTE_ARRAY_FIELD);
+ if (innerBytes == null) {
+ continue;
+ }
+ bytes[i] = innerBytes;
+ }
+ builder.setBytesValues(bytes);
+ valueSet = true;
+ } else if (docValues != null && docValues.length > 0) {
+ GenericDocumentParcel[] documentParcels =
+ new GenericDocumentParcel[docValues.length];
+ System.arraycopy(docValues, 0, documentParcels, 0, docValues.length);
+ builder.setDocumentValues(documentParcels);
+ valueSet = true;
+ } else if (embeddingArray != null) {
+ EmbeddingVector[] embeddings = new EmbeddingVector[embeddingArray.size()];
+ for (int i = 0; i < embeddingArray.size(); i++) {
+ Bundle embeddingBundle = embeddingArray.get(i);
+ if (embeddingBundle == null) {
+ continue;
+ }
+ float[] values = embeddingBundle.getFloatArray(EMBEDDING_VALUE_FIELD);
+ String modelSignature = embeddingBundle.getString(EMBEDDING_MODEL_SIGNATURE_FIELD);
+ if (values == null || modelSignature == null) {
+ continue;
+ }
+ embeddings[i] = new EmbeddingVector(values, modelSignature);
+ }
+ builder.setEmbeddingValues(embeddings);
+ valueSet = true;
+ }
+
+ if (!valueSet) {
+ throw new IllegalArgumentException("property bundle passed in doesn't have any "
+ + "value set.");
+ }
+
+ return builder.build();
+ }
+
+ /** Creates a {@link Bundle} from a {@link PropertyParcel}. */
+ @NonNull
+ private static Bundle createBundleFromPropertyParcel(
+ @NonNull PropertyParcel propertyParcel) {
+ Objects.requireNonNull(propertyParcel);
+ Bundle propertyParcelBundle = new Bundle();
+ propertyParcelBundle.putString(PROPERTY_NAME_FIELD, propertyParcel.getPropertyName());
+
+ // Check and set the properties
+ String[] stringValues = propertyParcel.getStringValues();
+ long[] longValues = propertyParcel.getLongValues();
+ double[] doubleValues = propertyParcel.getDoubleValues();
+ boolean[] booleanValues = propertyParcel.getBooleanValues();
+ byte[][] bytesArray = propertyParcel.getBytesValues();
+ GenericDocumentParcel[] docArray = propertyParcel.getDocumentValues();
+ EmbeddingVector[] embeddingArray = propertyParcel.getEmbeddingValues();
+
+ if (stringValues != null) {
+ propertyParcelBundle.putStringArray(STRING_ARRAY_FIELD, stringValues);
+ } else if (longValues != null) {
+ propertyParcelBundle.putLongArray(LONG_ARRAY_FIELD, longValues);
+ } else if (doubleValues != null) {
+ propertyParcelBundle.putDoubleArray(DOUBLE_ARRAY_FIELD, doubleValues);
+ } else if (booleanValues != null) {
+ propertyParcelBundle.putBooleanArray(BOOLEAN_ARRAY_FIELD, booleanValues);
+ } else if (bytesArray != null) {
+ ArrayList<Bundle> bundles = new ArrayList<>(bytesArray.length);
+ for (int i = 0; i < bytesArray.length; i++) {
+ Bundle byteArray = new Bundle();
+ byteArray.putByteArray(BYTE_ARRAY_FIELD, bytesArray[i]);
+ bundles.add(byteArray);
+ }
+ propertyParcelBundle.putParcelableArrayList(BYTES_ARRAY_FIELD, bundles);
+ } else if (docArray != null) {
+ propertyParcelBundle.putParcelableArray(DOC_ARRAY_FIELD, docArray);
+ } else if (embeddingArray != null) {
+ ArrayList<Bundle> bundles = new ArrayList<>(embeddingArray.length);
+ for (int i = 0; i < embeddingArray.length; i++) {
+ Bundle embedding = new Bundle();
+ embedding.putFloatArray(EMBEDDING_VALUE_FIELD, embeddingArray[i].getValues());
+ embedding.putString(EMBEDDING_MODEL_SIGNATURE_FIELD,
+ embeddingArray[i].getModelSignature());
+ bundles.add(embedding);
+ }
+ propertyParcelBundle.putParcelableArrayList(EMBEDDING_ARRAY_FIELD, bundles);
+ }
+
+ return propertyParcelBundle;
+ }
+
+ @NonNull
+ @Override
+ public PropertyParcel createFromParcel(Parcel in) {
+ Bundle bundle = in.readBundle(getClass().getClassLoader());
+ return createPropertyParcelFromBundle(bundle);
+ }
+
+ @Override
+ public PropertyParcel[] newArray(int size) {
+ return new PropertyParcel[size];
+ }
+
+ /** Writes a {@link PropertyParcel} to a {@link Parcel}. */
+ public static void writeToParcel(@NonNull PropertyParcel propertyParcel,
+ @NonNull android.os.Parcel parcel, int flags) {
+ parcel.writeBundle(createBundleFromPropertyParcel(propertyParcel));
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
index fa70911..8a25a36 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
@@ -74,4 +74,7 @@
*/
boolean doNotParcelTypeDefaultValues() default false;
}
+
+ /** Provide same interface as {@link android.os.Parcelable} for code sync purpose. */
+ int describeContents();
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
index 22755d0..53afaa7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
@@ -17,6 +17,7 @@
package androidx.appsearch.safeparcel.stub;
import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -31,7 +32,21 @@
*/
// @exportToFramework:skipFile()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class AbstractCreator {
+abstract class AbstractCreator<T> implements Parcelable.Creator<T> {
+ @Override
+ public T createFromParcel(Parcel var1) {
+ // This is here only for code sync purpose.
+ throw new UnsupportedOperationException("createFromParcel is not implemented and should "
+ + "not be used.");
+ }
+
+ @Override
+ public T[] newArray(int var1) {
+ // This is here only for code sync purpose.
+ throw new UnsupportedOperationException("newArray is not implemented and should "
+ + "not be used.");
+ }
+
public static void writeToParcel(
@NonNull SafeParcelable safeParcelable,
@NonNull Parcel parcel,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
index 97887bf..48d197c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
@@ -16,9 +16,34 @@
package androidx.appsearch.safeparcel.stub;
import androidx.annotation.RestrictTo;
-import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.JoinSpec;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResult.MatchInfo;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.SetSchemaResponse.MigrationFailure;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.observer.ObserverSpec;
import androidx.appsearch.safeparcel.PropertyConfigParcel;
-import androidx.appsearch.safeparcel.PropertyParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.EmbeddingIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.IntegerIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
+import androidx.appsearch.stats.SchemaMigrationStats;
/**
* Stub creators for any classes extending
@@ -29,61 +54,145 @@
* be provided for code sync purpose.
*/
// @exportToFramework:skipFile()
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class StubCreators {
/** Stub creator for {@link androidx.appsearch.app.StorageInfo}. */
- public static class StorageInfoCreator extends AbstractCreator {
- }
-
- /** Stub creator for {@link PropertyParcel}. */
- public static class PropertyParcelCreator extends AbstractCreator {
+ public static class StorageInfoCreator extends AbstractCreator<StorageInfo> {
}
/** Stub creator for {@link PropertyConfigParcel}. */
- public static class PropertyConfigParcelCreator extends AbstractCreator {
+ public static class PropertyConfigParcelCreator extends AbstractCreator<PropertyConfigParcel> {
}
/**
* Stub creator for
* {@link PropertyConfigParcel.JoinableConfigParcel}.
*/
- public static class JoinableConfigParcelCreator extends AbstractCreator {
+ public static class JoinableConfigParcelCreator extends AbstractCreator<JoinableConfigParcel> {
}
/**
* Stub creator for
* {@link PropertyConfigParcel.StringIndexingConfigParcel}.
*/
- public static class StringIndexingConfigParcelCreator extends AbstractCreator {
+ public static class StringIndexingConfigParcelCreator extends
+ AbstractCreator<StringIndexingConfigParcel> {
}
/**
* Stub creator for
* {@link PropertyConfigParcel.IntegerIndexingConfigParcel}.
*/
- public static class IntegerIndexingConfigParcelCreator extends AbstractCreator {
+ public static class IntegerIndexingConfigParcelCreator extends
+ AbstractCreator<IntegerIndexingConfigParcel> {
}
/**
* Stub creator for
* {@link PropertyConfigParcel.DocumentIndexingConfigParcel}.
*/
- public static class DocumentIndexingConfigParcelCreator extends AbstractCreator {
+ public static class DocumentIndexingConfigParcelCreator extends
+ AbstractCreator<DocumentIndexingConfigParcel> {
}
- /** Stub creator for {@link GenericDocumentParcel}. */
- public static class GenericDocumentParcelCreator extends AbstractCreator {
+ /** Stub creator for {@link SchemaVisibilityConfig}. */
+ public static class VisibilityConfigCreator extends AbstractCreator<SchemaVisibilityConfig> {
}
- /** Stub creator for {@link androidx.appsearch.app.VisibilityPermissionDocument}. */
- public static class VisibilityPermissionDocumentCreator extends AbstractCreator {
+ /**
+ * Stub creator for {@link EmbeddingIndexingConfigParcel}.
+ */
+ public static class EmbeddingIndexingConfigParcelCreator extends
+ AbstractCreator<EmbeddingIndexingConfigParcel> {
}
- /** Stub creator for {@link androidx.appsearch.app.VisibilityDocument}. */
- public static class VisibilityDocumentCreator extends AbstractCreator {
+ /** Stub creator for {@link InternalVisibilityConfig}. */
+ public static class InternalVisibilityConfigCreator
+ extends AbstractCreator<InternalVisibilityConfig> {
+ }
+
+ /** Stub creator for {@link VisibilityPermissionConfig}. */
+ public static class VisibilityPermissionConfigCreator extends
+ AbstractCreator<VisibilityPermissionConfig> {
}
/** Stub creator for {@link androidx.appsearch.stats.SchemaMigrationStats}. */
- public static class SchemaMigrationStatsCreator extends AbstractCreator {
+ public static class SchemaMigrationStatsCreator extends AbstractCreator<SchemaMigrationStats> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionResult}. */
+ public static class SearchSuggestionResultCreator extends
+ AbstractCreator<SearchSuggestionResult> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionSpec}. */
+ public static class SearchSuggestionSpecCreator extends AbstractCreator<SearchSuggestionSpec> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.observer.ObserverSpec}. */
+ public static class ObserverSpecCreator extends AbstractCreator<ObserverSpec> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse}. */
+ public static class SetSchemaResponseCreator extends
+ AbstractCreator<SetSchemaResponse> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}. */
+ public static class MigrationFailureCreator extends
+ AbstractCreator<MigrationFailure> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.InternalSetSchemaResponse}. */
+ public static class InternalSetSchemaResponseCreator extends
+ AbstractCreator<InternalSetSchemaResponse> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SearchSpec}. */
+ public static class SearchSpecCreator extends AbstractCreator<SearchSpec> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.JoinSpec}. */
+ public static class JoinSpecCreator extends AbstractCreator<JoinSpec> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.GetSchemaResponse}. */
+ public static class GetSchemaResponseCreator extends AbstractCreator<GetSchemaResponse> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.AppSearchSchema}. */
+ public static class AppSearchSchemaCreator extends
+ AbstractCreator<AppSearchSchema> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SearchResult}. */
+ public static class SearchResultCreator extends AbstractCreator<SearchResult> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.MatchInfo}. */
+ public static class MatchInfoCreator extends AbstractCreator<MatchInfo> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.SearchResultPage}. */
+ public static class SearchResultPageCreator extends AbstractCreator<SearchResultPage> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.RemoveByDocumentIdRequest}. */
+ public static class RemoveByDocumentIdRequestCreator extends
+ AbstractCreator<RemoveByDocumentIdRequest> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.ReportUsageRequest}. */
+ public static class ReportUsageRequestCreator extends AbstractCreator<ReportUsageRequest> {
+ }
+
+ /** Stub creator for {@link androidx.appsearch.app.GetByDocumentIdRequest}. */
+ public static class GetByDocumentIdRequestCreator extends
+ AbstractCreator<GetByDocumentIdRequest> {
+ }
+
+ /** Stub creator for {@link EmbeddingVector}. */
+ public static class EmbeddingVectorCreator extends
+ AbstractCreator<EmbeddingVector> {
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
index 0a24293..91b74e7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
@@ -17,6 +17,7 @@
package androidx.appsearch.stats;
import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@@ -40,10 +41,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SafeParcelable.Class(creator = "SchemaMigrationStatsCreator")
public final class SchemaMigrationStats extends AbstractSafeParcelable {
- @NonNull public static final SchemaMigrationStatsCreator CREATOR =
+ @NonNull public static final Parcelable.Creator<SchemaMigrationStats> CREATOR =
new SchemaMigrationStatsCreator();
- // Indicate the how a SetSchema call relative to SchemaMigration case.
+ /** Indicate the SetSchema call type relative to SchemaMigration case. */
@IntDef(
value = {
NO_MIGRATION,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
new file mode 100644
index 0000000..0fadd2c
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Wrapper class for action constants.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ActionConstants {
+ /**
+ * Unknown action type.
+ *
+ * <p>It is defined for abstract action class and compatibility, so it should not be used in any
+ * concrete instances.
+ */
+ public static final int ACTION_TYPE_UNKNOWN = 0;
+
+ /**
+ * Search action type.
+ *
+ * <!--@exportToFramework:ifJetpack()-->
+ * <p>It is the action type for {@link SearchAction}.
+ * <!--@exportToFramework:else()-->
+ */
+ public static final int ACTION_TYPE_SEARCH = 1;
+
+ /**
+ * Click action type.
+ *
+ * <!--@exportToFramework:ifJetpack()-->
+ * <p>It is the action type for {@link ClickAction}.
+ * <!--@exportToFramework:else()-->
+ */
+ public static final int ACTION_TYPE_CLICK = 2;
+
+ private ActionConstants() {}
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
new file mode 100644
index 0000000..e69320a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.Features;
+import androidx.core.util.Preconditions;
+
+/**
+ * {@link ClickAction} is a built-in AppSearch document type that contains different metrics.
+ * Clients can report the user's click actions on a {@link androidx.appsearch.app.SearchResult}
+ * document.
+ *
+ * <p>In order to use this document type, the client must explicitly set this schema type via
+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}.
+ *
+ * <p>Click actions can be used as signals to boost ranking via
+ * {@link androidx.appsearch.app.JoinSpec} API in future search requests.
+ *
+ * <p>Since {@link ClickAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The
+ * default TTL is 60 days.
+ */
+// In ClickAction document, there is a joinable property "referencedQualifiedId" for reporting the
+// qualified id of the clicked document. The client can create personal navboost with click action
+// signals by join query with this property. Therefore, ClickAction document class requires join
+// feature.
+@RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
+@Document(name = "builtin:ClickAction")
+public class ClickAction extends TakenAction {
+ @Nullable
+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ private final String mQuery;
+
+ @Nullable
+ @Document.StringProperty(joinableValueType =
+ StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ private final String mReferencedQualifiedId;
+
+ @Document.LongProperty
+ private final int mResultRankInBlock;
+
+ @Document.LongProperty
+ private final int mResultRankGlobal;
+
+ @Document.LongProperty
+ private final long mTimeStayOnResultMillis;
+
+ ClickAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+ long actionTimestampMillis, @TakenAction.ActionType int actionType,
+ @Nullable String query, @Nullable String referencedQualifiedId, int resultRankInBlock,
+ int resultRankGlobal, long timeStayOnResultMillis) {
+ super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);
+
+ mQuery = query;
+ mReferencedQualifiedId = referencedQualifiedId;
+ mResultRankInBlock = resultRankInBlock;
+ mResultRankGlobal = resultRankGlobal;
+ mTimeStayOnResultMillis = timeStayOnResultMillis;
+ }
+
+ /**
+ * Returns the user-entered search input (without any operators or rewriting) that yielded the
+ * {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+ */
+ @Nullable
+ public String getQuery() {
+ return mQuery;
+ }
+
+ /**
+ * Returns the qualified id of the {@link androidx.appsearch.app.SearchResult} document that the
+ * user clicked on.
+ *
+ * <p>A qualified id is a string generated by package, database, namespace, and document id. See
+ * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(String,String,String,String)}
+ * for more details.
+ */
+ @Nullable
+ public String getReferencedQualifiedId() {
+ return mReferencedQualifiedId;
+ }
+
+ /**
+ * Returns the rank of the {@link androidx.appsearch.app.SearchResult} document among the
+ * user-defined block.
+ *
+ * <p>The client can define its own custom definition for block, e.g. corpus name, group, etc.
+ *
+ * <p>For example, a client defines the block as corpus, and AppSearch returns 5 documents with
+ * corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"]. Then the block ranks of
+ * them = [1, 2, 1, 1, 2].
+ *
+ * <p>If the client is not presenting the results in multiple blocks, they should set this value
+ * to match {@link #getResultRankGlobal}.
+ *
+ * <p>If unset, then the block rank of the {@link androidx.appsearch.app.SearchResult} document
+ * will be set to -1 to mark invalid.
+ */
+ public int getResultRankInBlock() {
+ return mResultRankInBlock;
+ }
+
+ /**
+ * Returns the global rank of the {@link androidx.appsearch.app.SearchResult} document.
+ *
+ * <p>Global rank reflects the order of {@link androidx.appsearch.app.SearchResult} documents
+ * returned by AppSearch.
+ *
+ * <p>For example, AppSearch returns 2 pages with 10 {@link androidx.appsearch.app.SearchResult}
+ * documents for each page. Then the global ranks of them will be 1 to 10 for the first page,
+ * and 11 to 20 for the second page.
+ *
+ * <p>If unset, then the global rank of the {@link androidx.appsearch.app.SearchResult} document
+ * will be set to -1 to mark invalid.
+ */
+ public int getResultRankGlobal() {
+ return mResultRankGlobal;
+ }
+
+ /**
+ * Returns the time in milliseconds that user stays on the
+ * {@link androidx.appsearch.app.SearchResult} document after clicking it.
+ */
+ public long getTimeStayOnResultMillis() {
+ return mTimeStayOnResultMillis;
+ }
+
+ // TODO(b/314026345): redesign builder to enable inheritance for ClickAction.
+ /** Builder for {@link ClickAction}. */
+ @Document.BuilderProducer
+ public static final class Builder extends BuilderImpl<Builder> {
+ private String mQuery;
+ private String mReferencedQualifiedId;
+ private int mResultRankInBlock;
+ private int mResultRankGlobal;
+ private long mTimeStayOnResultMillis;
+
+ /**
+ * Constructor for {@link ClickAction.Builder}.
+ *
+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.
+ * @param id Unique identifier for the Document. See {@link Document.Id}.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {
+ this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_CLICK);
+ }
+
+ /**
+ * Constructs {@link ClickAction.Builder} by copying existing values from the given
+ * {@link ClickAction}.
+ *
+ * @param clickAction an existing {@link ClickAction} object.
+ */
+ public Builder(@NonNull ClickAction clickAction) {
+ super(Preconditions.checkNotNull(clickAction));
+
+ mQuery = clickAction.getQuery();
+ mReferencedQualifiedId = clickAction.getReferencedQualifiedId();
+ mResultRankInBlock = clickAction.getResultRankInBlock();
+ mResultRankGlobal = clickAction.getResultRankGlobal();
+ mTimeStayOnResultMillis = clickAction.getTimeStayOnResultMillis();
+ }
+
+ /**
+ * Constructor for {@link ClickAction.Builder}.
+ *
+ * <p>It is required by {@link Document.BuilderProducer}.
+ *
+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.
+ * @param id Unique identifier for the Document. See {@link Document.Id}.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ * @param actionType Action type enum for the Document. See
+ * {@link TakenAction.ActionType}.
+ */
+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+ @TakenAction.ActionType int actionType) {
+ super(namespace, id, actionTimestampMillis, actionType);
+
+ // Default for unset result rank fields. Since negative number is invalid for ranking,
+ // -1 is used as an unset value and AppSearch will ignore it.
+ mResultRankInBlock = -1;
+ mResultRankGlobal = -1;
+
+ // Default for unset timeStayOnResultMillis. Since negative number is invalid for
+ // time in millis, -1 is used as an unset value and AppSearch will ignore it.
+ mTimeStayOnResultMillis = -1;
+ }
+
+ /**
+ * Sets the user-entered search input (without any operators or rewriting) that yielded
+ * the {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQuery(@Nullable String query) {
+ mQuery = query;
+ return this;
+ }
+
+ /**
+ * Sets the qualified id of the {@link androidx.appsearch.app.SearchResult} document that
+ * the user takes action on.
+ *
+ * <p>A qualified id is a string generated by package, database, namespace, and document id.
+ * See {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(
+ * String,String,String,String)} for more details.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setReferencedQualifiedId(@Nullable String referencedQualifiedId) {
+ mReferencedQualifiedId = referencedQualifiedId;
+ return this;
+ }
+
+ /**
+ * Sets the rank of the {@link androidx.appsearch.app.SearchResult} document among the
+ * user-defined block.
+ *
+ * @see ClickAction#getResultRankInBlock
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankInBlock(int resultRankInBlock) {
+ mResultRankInBlock = resultRankInBlock;
+ return this;
+ }
+
+ /**
+ * Sets the global rank of the {@link androidx.appsearch.app.SearchResult} document.
+ *
+ * @see ClickAction#getResultRankGlobal
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setResultRankGlobal(int resultRankGlobal) {
+ mResultRankGlobal = resultRankGlobal;
+ return this;
+ }
+
+ /**
+ * Sets the time in milliseconds that user stays on the
+ * {@link androidx.appsearch.app.SearchResult} document after clicking it.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+ mTimeStayOnResultMillis = timeStayOnResultMillis;
+ return this;
+ }
+
+ /** Builds a {@link ClickAction}. */
+ @Override
+ @NonNull
+ public ClickAction build() {
+ return new ClickAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,
+ mActionType, mQuery, mReferencedQualifiedId, mResultRankInBlock,
+ mResultRankGlobal, mTimeStayOnResultMillis);
+ }
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
new file mode 100644
index 0000000..67cb4ea
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * {@link SearchAction} is a built-in AppSearch document type that contains different metrics.
+ * <ul>
+ * <li>Clients can report the user's search actions.
+ * <li>Usually {@link SearchAction} is reported together with {@link ClickAction}, since the
+ * user clicks on {@link androidx.appsearch.app.SearchResult} documents after searching.
+ * </ul>
+ *
+ * <p>In order to use this document type, the client must explicitly set this schema type via
+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}.
+ *
+ * <p>Since {@link SearchAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The
+ * default TTL is 60 days.
+ */
+@Document(name = "builtin:SearchAction")
+public class SearchAction extends TakenAction {
+ @Nullable
+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ private final String mQuery;
+
+ @Document.LongProperty
+ private final int mFetchedResultCount;
+
+ SearchAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+ long actionTimestampMillis, @TakenAction.ActionType int actionType,
+ @Nullable String query, int fetchedResultCount) {
+ super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);
+
+ mQuery = query;
+ mFetchedResultCount = fetchedResultCount;
+ }
+
+ /** Returns the user-entered search input (without any operators or rewriting). */
+ @Nullable
+ public String getQuery() {
+ return mQuery;
+ }
+
+ /**
+ * Returns total number of results fetched from AppSearch by the client in this
+ * {@link SearchAction}.
+ *
+ * <p>If unset, then it will be set to -1 to mark invalid.
+ */
+ public int getFetchedResultCount() {
+ return mFetchedResultCount;
+ }
+
+ // TODO(b/314026345): redesign builder to enable inheritance for SearchAction.
+ /** Builder for {@link SearchAction}. */
+ @Document.BuilderProducer
+ public static final class Builder extends BuilderImpl<Builder> {
+ private String mQuery;
+ private int mFetchedResultCount;
+
+ /**
+ * Constructor for {@link SearchAction.Builder}.
+ *
+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.
+ * @param id Unique identifier for the Document. See {@link Document.Id}.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {
+ this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_SEARCH);
+ }
+
+ /**
+ * Constructor for {@link Builder} with all the existing values.
+ */
+ public Builder(@NonNull SearchAction searchAction) {
+ super(Preconditions.checkNotNull(searchAction));
+
+ mQuery = searchAction.getQuery();
+ mFetchedResultCount = searchAction.getFetchedResultCount();
+ }
+
+ /**
+ * Constructor for {@link SearchAction.Builder}.
+ *
+ * <p>It is required by {@link Document.BuilderProducer}.
+ *
+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.
+ * @param id Unique identifier for the Document. See {@link Document.Id}.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ * @param actionType Action type enum for the Document. See
+ * {@link TakenAction.ActionType}.
+ */
+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+ @TakenAction.ActionType int actionType) {
+ super(namespace, id, actionTimestampMillis, actionType);
+
+ // Default for unset fetchedResultCount. Since negative number is invalid for fetched
+ // result count, -1 is used as an unset value and AppSearch will ignore it.
+ mFetchedResultCount = -1;
+ }
+
+ /** Sets the user-entered search input (without any operators or rewriting). */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQuery(@Nullable String query) {
+ mQuery = query;
+ return this;
+ }
+
+ /**
+ * Sets total number of results fetched from AppSearch by the client in this
+ * {@link SearchAction}.
+ *
+ * @see SearchAction#getFetchedResultCount
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setFetchedResultCount(int fetchedResultCount) {
+ mFetchedResultCount = fetchedResultCount;
+ return this;
+ }
+
+ /** Builds a {@link SearchAction}. */
+ @Override
+ @NonNull
+ public SearchAction build() {
+ return new SearchAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,
+ mActionType, mQuery, mFetchedResultCount);
+ }
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
new file mode 100644
index 0000000..da35743
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * {@link TakenAction} is an abstract class which holds common fields of other AppSearch built-in
+ * action types (e.g. {@link SearchAction}, {@link ClickAction}).
+ *
+ * <p>Clients can report the user's actions by creating concrete actions with
+ * {@link androidx.appsearch.app.PutDocumentsRequest.Builder#addTakenActions} API.
+ */
+@Document(name = "builtin:TakenAction")
+public abstract class TakenAction {
+ /** Default TTL for all related {@link TakenAction} documents: 60 days. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final long DEFAULT_DOCUMENT_TTL_MILLIS = 60L * 24 * 60 * 60 * 1000;
+
+ /** AppSearch taken action type. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef(value = {
+ ActionConstants.ACTION_TYPE_UNKNOWN,
+ ActionConstants.ACTION_TYPE_SEARCH,
+ ActionConstants.ACTION_TYPE_CLICK,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionType {
+ }
+
+ @NonNull
+ @Document.Namespace
+ private final String mNamespace;
+
+ @NonNull
+ @Document.Id
+ private final String mId;
+
+ @Document.TtlMillis
+ private final long mDocumentTtlMillis;
+
+ @Document.CreationTimestampMillis
+ private final long mActionTimestampMillis;
+
+ @Document.LongProperty
+ @ActionType
+ private final int mActionType;
+
+ TakenAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+ long actionTimestampMillis, @ActionType int actionType) {
+ mNamespace = Preconditions.checkNotNull(namespace);
+ mId = Preconditions.checkNotNull(id);
+ mDocumentTtlMillis = documentTtlMillis;
+ mActionTimestampMillis = actionTimestampMillis;
+ mActionType = actionType;
+ }
+
+ /** Returns the namespace of the {@link TakenAction}. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the unique identifier of the {@link TakenAction}. */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the time-to-live (TTL) of the {@link TakenAction} document as a duration in
+ * milliseconds.
+ *
+ * <p>The document will be automatically deleted when the TTL expires (since
+ * {@link #getActionTimestampMillis()}).
+ *
+ * <p>The default TTL for {@link TakenAction} document is 60 days.
+ *
+ * <p>See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on TTL.
+ */
+ public long getDocumentTtlMillis() {
+ return mDocumentTtlMillis;
+ }
+
+ /**
+ * Returns the timestamp when the user took the action, in milliseconds since Unix epoch.
+ *
+ * <p>The action timestamp will be used together with {@link #getDocumentTtlMillis()} as the
+ * document retention.
+ */
+ public long getActionTimestampMillis() {
+ return mActionTimestampMillis;
+ }
+
+ /**
+ * Returns the action type of the {@link TakenAction}.
+ *
+ * @see TakenAction.ActionType
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @ActionType
+ public int getActionType() {
+ return mActionType;
+ }
+
+ // TODO(b/330777270): improve AnnotationProcessor for abstract document class, and remove this
+ // builder.
+ /** Builder for {@link TakenAction}. */
+ @Document.BuilderProducer
+ static final class Builder extends BuilderImpl<Builder> {
+ /**
+ * Constructor for {@link TakenAction.Builder}.
+ *
+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.
+ * @param id Unique identifier for the Document. See {@link Document.Id}.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ * @param actionType Action type enum for the Document. See
+ * {@link TakenAction.ActionType}.
+ */
+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+ @TakenAction.ActionType int actionType) {
+ super(namespace, id, actionTimestampMillis, actionType);
+ }
+
+ /** Constructor for {@link TakenAction.Builder} with all the existing values. */
+ Builder(@NonNull TakenAction takenAction) {
+ super(takenAction);
+ }
+ }
+
+ // Use templated BuilderImpl to resolve base class setter return type issue for child class
+ // builder instances.
+ @SuppressWarnings("unchecked")
+ static class BuilderImpl<T extends BuilderImpl<T>> {
+ protected final String mNamespace;
+ protected final String mId;
+ protected long mDocumentTtlMillis;
+ protected long mActionTimestampMillis;
+ @ActionType
+ protected int mActionType;
+
+ /**
+ * Constructs {@link TakenAction.BuilderImpl} with given {@code namespace}, {@code id},
+ * {@code actionTimestampMillis} and {@code actionType}.
+ *
+ * @param namespace The namespace of the {@link TakenAction} document.
+ * @param id The id of the {@link TakenAction} document.
+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+ * since Unix epoch.
+ * @param actionType The action type enum of the Document.
+ */
+ BuilderImpl(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+ @TakenAction.ActionType int actionType) {
+ mNamespace = Preconditions.checkNotNull(namespace);
+ mId = Preconditions.checkNotNull(id);
+ mActionTimestampMillis = actionTimestampMillis;
+ mActionType = actionType;
+
+ // Default for documentTtlMillis.
+ mDocumentTtlMillis = TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS;
+ }
+
+ /**
+ * Constructs {@link TakenAction.BuilderImpl} by copying existing values from the given
+ * {@link TakenAction}.
+ *
+ * @param takenAction an existing {@link TakenAction} object.
+ */
+ BuilderImpl(@NonNull TakenAction takenAction) {
+ this(takenAction.getNamespace(), takenAction.getId(),
+ takenAction.getActionTimestampMillis(), takenAction.getActionType());
+ mDocumentTtlMillis = takenAction.getDocumentTtlMillis();
+ }
+
+ /**
+ * Sets the time-to-live (TTL) of the {@link TakenAction} document as a duration in
+ * milliseconds.
+ *
+ * <p>The document will be automatically deleted when the TTL expires (since
+ * {@link TakenAction#getActionTimestampMillis()}).
+ *
+ * <p>The default TTL for {@link TakenAction} document is 60 days.
+ *
+ * <p>See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on
+ * TTL.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public T setDocumentTtlMillis(long documentTtlMillis) {
+ mDocumentTtlMillis = documentTtlMillis;
+ return (T) this;
+ }
+
+ // TODO(b/330777270): improve AnnotationProcessor for abstract document class builder, and
+ // make it an abstract method.
+ /**
+ * For AppSearch annotation processor requirement only. The client should never call it
+ * since it is impossible to instantiate an abstract class.
+ *
+ * @throws UnsupportedOperationException
+ */
+ @NonNull
+ public TakenAction build() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
index 9d50086..7604db5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -248,7 +248,7 @@
// Read bundle from bytes
parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
parcel.setDataPosition(0);
- return parcel.readBundle();
+ return parcel.readBundle(BundleUtil.class.getClassLoader());
} finally {
parcel.recycle();
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
new file mode 100644
index 0000000..c09ac7a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.util;
+
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utilities for handling exceptions.
+ */
+// This file has different behavior in Framework as compared to JetPack (like it is okay to rethrow
+// exception and log instead of rethrowing from SystemServer in case of RemoteException). This file
+// is not synced to Framework, as it maintains its own environment specific copy.
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ExceptionUtil {
+ private static final String TAG = "AppSearchExceptionUtil";
+
+ /**
+ * {@link RuntimeException} will be rethrown if {@link #isItOkayToRethrowException()} returns
+ * true.
+ */
+ public static void handleException(@NonNull Exception e) {
+ if (isItOkayToRethrowException() && e instanceof RuntimeException) {
+ rethrowRuntimeException((RuntimeException) e);
+ }
+ }
+
+ /** Returns whether it is OK to rethrow exceptions from this entrypoint. */
+ private static boolean isItOkayToRethrowException() {
+ return true;
+ }
+
+ /** Rethrow exception from SystemServer in Framework code. */
+ public static void handleRemoteException(@NonNull RemoteException e) {
+ Log.w(TAG, "Unable to make a call to AppSearchManagerService!", e);
+ }
+
+ /**
+ * A helper method to rethrow {@link RuntimeException}.
+ *
+ * <p>We use this to enforce exception type and assure the compiler/linter that the exception is
+ * indeed {@link RuntimeException} and can be rethrown safely.
+ */
+ private static void rethrowRuntimeException(RuntimeException e) {
+ throw e;
+ }
+
+ private ExceptionUtil() {}
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
index cc52cdd..0f3e100 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
@@ -22,6 +22,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.Size;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
/**
* Utilities for logging to logcat.
@@ -33,6 +34,8 @@
// TODO(b/232285376): If it becomes possible to detect an eng build, turn this on by default
// for eng builds.
public static final boolean DEBUG = false;
+ public static final boolean INFO = AppSearchEnvironmentFactory.getEnvironmentInstance()
+ .isInfoLoggingEnabled();
/**
* The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in
@@ -92,7 +95,7 @@
@NonNull String message,
@Nullable Object fastTraceObj,
@Nullable Object fullTraceObj) {
- if (PII_TRACE_LEVEL == 0) {
+ if (PII_TRACE_LEVEL == 0 || !INFO) {
return;
}
StringBuilder builder = new StringBuilder("(trace) ").append(message);
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
index dd8ae1d..39dd6c7 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
@@ -527,6 +527,13 @@
env,
/* allowRepeated= */true);
break;
+ case EMBEDDING_PROPERTY:
+ requireTypeIsOneOf(
+ getterOrField,
+ List.of(helper.mEmbeddingType),
+ env,
+ /* allowRepeated= */true);
+ break;
default:
throw new IllegalStateException("Unhandled annotation: " + annotation);
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 9f11508..7296edd 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -203,8 +203,9 @@
// unboxing.
//
// 1b: ListCallArraysAsList
- // List contains String. We have to convert this from an array of String[], but no
- // conversion of the collection elements is needed. We can use Arrays#asList for this.
+ // List contains String or EmbeddingVector. We have to convert this from
+ // an array of String[] or EmbeddingVector[], but no conversion of the
+ // collection elements is needed. We can use Arrays#asList for this.
//
// 1c: ListForLoopCallFromGenericDocument
// List contains a class which is annotated with @Document.
@@ -225,7 +226,8 @@
// of unboxing.
//
// 2b: ArrayUseDirectly
- // Array is of type String[], long[], double[], boolean[], byte[][].
+ // Array is of type String[], long[], double[], boolean[], byte[][] or
+ // EmbeddingVector[].
// We can directly use this field with no conversion.
//
// 2c: ArrayForLoopCallFromGenericDocument
@@ -243,7 +245,8 @@
// Scenario 3: Single valued fields
// 3a: FieldUseDirectlyWithNullCheck
- // Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
+ // Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
+ // EmbeddingVector.
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
//
@@ -393,6 +396,19 @@
default:
throw new IllegalStateException("Unhandled type-category: " + typeCategory);
}
+ case EMBEDDING_PROPERTY:
+ switch (typeCategory) {
+ case COLLECTION: // List<EmbeddingVector>: 1b
+ return listCallArraysAsList(annotation, getterOrField);
+ case ARRAY:
+ // EmbeddingVector[]: 2b
+ return arrayUseDirectly(annotation, getterOrField);
+ case SINGLE:
+ // EmbeddingVector: 3a
+ return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+ default:
+ throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+ }
default:
throw new IllegalStateException("Unhandled annotation: " + annotation);
}
@@ -433,8 +449,9 @@
}
// 1b: ListCallArraysAsList
- // List contains String. We have to convert this from an array of String[], but no
- // conversion of the collection elements is needed. We can use Arrays#asList for this.
+ // List contains String or EmbeddingVector. We have to convert this from
+ // an array of String[] or EmbeddingVector[], but no conversion of the
+ // collection elements is needed. We can use Arrays#asList for this.
@NonNull
private CodeBlock listCallArraysAsList(
@NonNull DataPropertyAnnotation annotation,
@@ -559,8 +576,8 @@
}
// 2b: ArrayUseDirectly
- // Array is of type String[], long[], double[], boolean[], byte[][].
- // We can directly use this field with no conversion.
+ // Array is of type String[], long[], double[], boolean[], byte[][] or
+ // EmbeddingVector[].
@NonNull
private CodeBlock arrayUseDirectly(
@NonNull DataPropertyAnnotation annotation,
@@ -646,7 +663,8 @@
}
// 3a: FieldUseDirectlyWithNullCheck
- // Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
+ // Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
+ // EmbeddingVector.
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
@NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 8ad865d..dba768a 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -90,6 +90,9 @@
public static final ClassName GENERIC_DOCUMENT_CLASS =
ClassName.get(APPSEARCH_PKG, "GenericDocument");
+ public static final ClassName EMBEDDING_VECTOR_CLASS =
+ ClassName.get(APPSEARCH_PKG, "EmbeddingVector");
+
public static final ClassName BUILDER_PRODUCER_CLASS =
DOCUMENT_ANNOTATION_CLASS.nestedClass("BuilderProducer");
@@ -108,6 +111,7 @@
public final TypeMirror mBooleanPrimitiveType;
public final TypeMirror mBytePrimitiveArrayType;
public final TypeMirror mGenericDocumentType;
+ public final TypeMirror mEmbeddingType;
public final TypeMirror mDoublePrimitiveType;
final TypeMirror mCollectionType;
final TypeMirror mListType;
@@ -151,6 +155,8 @@
mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
mGenericDocumentType =
mElementUtils.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();
+ mEmbeddingType = mElementUtils.getTypeElement(
+ EMBEDDING_VECTOR_CLASS.canonicalName()).asType();
}
/**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 9199c47..42c6b31 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -28,6 +28,7 @@
import androidx.annotation.NonNull;
import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
import androidx.appsearch.compiler.annotationwrapper.DocumentPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.EmbeddingPropertyAnnotation;
import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation;
import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
@@ -229,6 +230,12 @@
LongPropertyAnnotation longPropertyAnnotation = (LongPropertyAnnotation) annotation;
codeBlock.add(createSetIndexingTypeExpr(longPropertyAnnotation, getterOrField));
break;
+ case EMBEDDING_PROPERTY:
+ EmbeddingPropertyAnnotation embeddingPropertyAnnotation =
+ (EmbeddingPropertyAnnotation) annotation;
+ codeBlock.add(
+ createSetIndexingTypeExpr(embeddingPropertyAnnotation, getterOrField));
+ break;
case DOUBLE_PROPERTY: // fall-through
case BOOLEAN_PROPERTY: // fall-through
case BYTES_PROPERTY:
@@ -418,6 +425,31 @@
/**
* Creates an expr like
+ * {@code .setIndexingType(EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)}.
+ */
+ @NonNull
+ private static CodeBlock createSetIndexingTypeExpr(
+ @NonNull EmbeddingPropertyAnnotation annotation,
+ @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+ String enumName;
+ switch (annotation.getIndexingType()) {
+ case 0:
+ enumName = "INDEXING_TYPE_NONE";
+ break;
+ case 1:
+ enumName = "INDEXING_TYPE_SIMILARITY";
+ break;
+ default:
+ throw new ProcessingException(
+ "Unknown indexing type " + annotation.getIndexingType(),
+ getterOrField.getElement());
+ }
+ return CodeBlock.of("\n.setIndexingType($T.$N)",
+ EmbeddingPropertyAnnotation.CONFIG_CLASS, enumName);
+ }
+
+ /**
+ * Creates an expr like
* {@code .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)}.
*/
@NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index 96bba93..90ebb78 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -165,10 +165,10 @@
// care of unboxing and widening where necessary.
//
// 1b: CollectionCallToArray
- // Collection contains String or GenericDocument.
- // We have to convert this into an array of String[] or GenericDocument[], but no
- // conversion of the collection elements is needed. We can use Collection#toArray for
- // this.
+ // Collection contains String, GenericDocument or EmbeddingVector.
+ // We have to convert this into an array of String[], GenericDocument[] or
+ // EmbeddingVector[], but no conversion of the collection elements is
+ // needed. We can use Collection#toArray for this.
//
// 1c: CollectionForLoopCallToGenericDocument
// Collection contains a class which is annotated with @Document.
@@ -188,8 +188,8 @@
// unboxing and widening where necessary.
//
// 2b: ArrayUseDirectly
- // Array is of type String[], long[], double[], boolean[], byte[][] or
- // GenericDocument[].
+ // Array is of type String[], long[], double[], boolean[], byte[][],
+ // GenericDocument[] or EmbeddingVector[].
// We can directly use this field with no conversion.
//
// 2c: ArrayForLoopCallToGenericDocument
@@ -207,7 +207,8 @@
// Scenario 3: Single valued fields
// 3a: FieldUseDirectlyWithNullCheck
- // Field is of type String, Long, Integer, Double, Float, Boolean.
+ // Field is of type String, Long, Integer, Double, Float, Boolean or
+ // EmbeddingVector.
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
//
@@ -375,6 +376,20 @@
default:
throw new IllegalStateException("Unhandled type-category: " + typeCategory);
}
+ case EMBEDDING_PROPERTY:
+ switch (typeCategory) {
+ case COLLECTION:
+ // List<EmbeddingVector>: 1b
+ return collectionCallToArray(annotation, getterOrField);
+ case ARRAY:
+ // EmbeddingVector[]: 2b
+ return arrayUseDirectly(annotation, getterOrField);
+ case SINGLE:
+ // EmbeddingVector: 3a
+ return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+ default:
+ throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+ }
default:
throw new IllegalStateException("Unhandled annotation: " + annotation);
}
@@ -417,10 +432,10 @@
}
// 1b: CollectionCallToArray
- // Collection contains String or GenericDocument.
- // We have to convert this into an array of String[] or GenericDocument[], but no
- // conversion of the collection elements is needed. We can use Collection#toArray for
- // this.
+ // Collection contains String, GenericDocument or EmbeddingVector.
+ // We have to convert this into an array of String[], GenericDocument[] or
+ // EmbeddingVector[], but no conversion of the collection elements is
+ // needed. We can use Collection#toArray for this.
@NonNull
private CodeBlock collectionCallToArray(
@NonNull DataPropertyAnnotation annotation,
@@ -536,8 +551,8 @@
}
// 2b: ArrayUseDirectly
- // Array is of type String[], long[], double[], boolean[], byte[][] or
- // GenericDocument[].
+ // Array is of type String[], long[], double[], boolean[], byte[][],
+ // GenericDocument[] or EmbeddingVector[].
// We can directly use this field with no conversion.
@NonNull
private CodeBlock arrayUseDirectly(
@@ -613,7 +628,8 @@
}
// 3a: FieldUseDirectlyWithNullCheck
- // Field is of type String, Long, Integer, Double, Float, Boolean.
+ // Field is of type String, Long, Integer, Double, Float, Boolean or
+ // EmbeddingVector.
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
@NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
index 38621c4..1c78f19 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
@@ -38,12 +38,13 @@
* <li>{@link DoublePropertyAnnotation}</li>
* <li>{@link BooleanPropertyAnnotation}</li>
* <li>{@link BytesPropertyAnnotation}</li>
+ * <li>{@link EmbeddingPropertyAnnotation}</li>
* </ul>
*/
public abstract class DataPropertyAnnotation implements PropertyAnnotation {
public enum Kind {
STRING_PROPERTY, DOCUMENT_PROPERTY, LONG_PROPERTY, DOUBLE_PROPERTY, BOOLEAN_PROPERTY,
- BYTES_PROPERTY
+ BYTES_PROPERTY, EMBEDDING_PROPERTY
}
@NonNull
@@ -103,6 +104,9 @@
return LongPropertyAnnotation.parse(annotationParams, defaultName);
} else if (qualifiedClassName.equals(StringPropertyAnnotation.CLASS_NAME.canonicalName())) {
return StringPropertyAnnotation.parse(annotationParams, defaultName);
+ } else if (qualifiedClassName.equals(
+ EmbeddingPropertyAnnotation.CLASS_NAME.canonicalName())) {
+ return EmbeddingPropertyAnnotation.parse(annotationParams, defaultName);
}
return null;
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
new file mode 100644
index 0000000..882c737
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 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.compiler.annotationwrapper;
+
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.compiler.IntrospectionHelper;
+import androidx.appsearch.compiler.ProcessingException;
+
+import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
+
+import java.util.Map;
+
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * An instance of the {@code @Document.EmbeddingProperty} annotation.
+ */
+@AutoValue
+public abstract class EmbeddingPropertyAnnotation extends DataPropertyAnnotation {
+ public static final ClassName CLASS_NAME =
+ DOCUMENT_ANNOTATION_CLASS.nestedClass("EmbeddingProperty");
+
+ public static final ClassName CONFIG_CLASS =
+ APPSEARCH_SCHEMA_CLASS.nestedClass("EmbeddingPropertyConfig");
+
+ public EmbeddingPropertyAnnotation() {
+ super(
+ CLASS_NAME,
+ CONFIG_CLASS,
+ /* genericDocGetterName= */"getPropertyEmbedding",
+ /* genericDocArrayGetterName= */"getPropertyEmbeddingArray",
+ /* genericDocSetterName= */"setPropertyEmbedding");
+ }
+
+ /**
+ * @param defaultName The name to use for the annotated property in case the annotation
+ * params do not mention an explicit name.
+ * @throws ProcessingException If the annotation points to an Illegal serializer class.
+ */
+ @NonNull
+ static EmbeddingPropertyAnnotation parse(
+ @NonNull Map<String, Object> annotationParams,
+ @NonNull String defaultName) throws ProcessingException {
+ String name = (String) annotationParams.get("name");
+ return new AutoValue_EmbeddingPropertyAnnotation(
+ name.isEmpty() ? defaultName : name,
+ (boolean) annotationParams.get("required"),
+ (int) annotationParams.get("indexingType"));
+ }
+
+ /**
+ * Specifies how a property should be indexed.
+ */
+ public abstract int getIndexingType();
+
+ @NonNull
+ @Override
+ public final Kind getDataPropertyKind() {
+ return Kind.EMBEDDING_PROPERTY;
+ }
+
+ @NonNull
+ @Override
+ public TypeMirror getUnderlyingTypeWithinGenericDoc(@NonNull IntrospectionHelper helper) {
+ return helper.mEmbeddingType;
+ }
+}
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 57cbd28..fa7b7aa 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -1048,18 +1048,20 @@
// TODO(b/156296904): Uncomment Gift in this test when it's supported
Compilation compilation = compile(
"import java.util.*;\n"
+ + "import androidx.appsearch.app.EmbeddingVector;\n"
+ "@Document\n"
+ "public class Gift {\n"
+ " @Document.Namespace String namespace;\n"
+ " @Document.Id String id;\n"
- + " @Document.StringProperty String stringProp;\n"
- + " @Document.LongProperty Integer integerProp;\n"
- + " @Document.LongProperty Long longProp;\n"
- + " @Document.DoubleProperty Float floatProp;\n"
- + " @Document.DoubleProperty Double doubleProp;\n"
- + " @Document.BooleanProperty Boolean booleanProp;\n"
- + " @Document.BytesProperty byte[] bytesProp;\n"
- //+ " @Document.Property Gift documentProp;\n"
+ + " @StringProperty String stringProp;\n"
+ + " @LongProperty Integer integerProp;\n"
+ + " @LongProperty Long longProp;\n"
+ + " @DoubleProperty Float floatProp;\n"
+ + " @DoubleProperty Double doubleProp;\n"
+ + " @BooleanProperty Boolean booleanProp;\n"
+ + " @BytesProperty byte[] bytesProp;\n"
+ + " @EmbeddingProperty EmbeddingVector vectorProp;\n"
+ //+ " @DocumentProperty Gift documentProp;\n"
+ "}\n");
assertThat(compilation).succeededWithoutWarnings();
@@ -1260,6 +1262,7 @@
Compilation compilation = compile(
"import java.util.*;\n"
+ "import androidx.appsearch.app.GenericDocument;\n"
+ + "import androidx.appsearch.app.EmbeddingVector;\n"
+ "@Document\n"
+ "public class Gift {\n"
+ " @Namespace String namespace;\n"
@@ -1274,6 +1277,7 @@
+ " @BytesProperty Collection<byte[]> collectByteArr;\n" // 1a
+ " @StringProperty Collection<String> collectString;\n" // 1b
+ " @DocumentProperty Collection<Gift> collectGift;\n" // 1c
+ + " @EmbeddingProperty Collection<EmbeddingVector> collectVec;\n" // 1b
+ "\n"
+ " // Arrays\n"
+ " @LongProperty Long[] arrBoxLong;\n" // 2a
@@ -1289,6 +1293,7 @@
+ " @BytesProperty byte[][] arrUnboxByteArr;\n" // 2b
+ " @StringProperty String[] arrString;\n" // 2b
+ " @DocumentProperty Gift[] arrGift;\n" // 2c
+ + " @EmbeddingProperty EmbeddingVector[] arrVec;\n" // 2b
+ "\n"
+ " // Single values\n"
+ " @StringProperty String string;\n" // 3a
@@ -1304,6 +1309,7 @@
+ " @BooleanProperty boolean unboxBoolean;\n" // 3b
+ " @BytesProperty byte[] unboxByteArr;\n" // 3a
+ " @DocumentProperty Gift gift;\n" // 3c
+ + " @EmbeddingProperty EmbeddingVector vec;\n" // 3a
+ "}\n");
assertThat(compilation).succeededWithoutWarnings();
@@ -3480,6 +3486,38 @@
checkEqualsGolden("Gift.java");
}
+ @Test
+ public void testEmbeddingFields() throws Exception {
+ Compilation compilation = compile(
+ "import java.util.*;\n"
+ + "import androidx.appsearch.app.EmbeddingVector;\n"
+ + "@Document\n"
+ + "public class Gift {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + " @Document.StringProperty String name;\n"
+ // Embedding properties
+ + " @EmbeddingProperty EmbeddingVector defaultIndexNone;\n"
+ + " @EmbeddingProperty(indexingType=0) EmbeddingVector indexNone;\n"
+ + " @EmbeddingProperty(indexingType=1) EmbeddingVector vec;\n"
+ + " @EmbeddingProperty(indexingType=1) List<EmbeddingVector> listVec;\n"
+ + " @EmbeddingProperty(indexingType=1)"
+ + " Collection<EmbeddingVector> collectVec;\n"
+ + " @EmbeddingProperty(indexingType=1) EmbeddingVector[] arrVec;\n"
+ + "}\n");
+
+ assertThat(compilation).succeededWithoutWarnings();
+ checkResultContains("Gift.java",
+ "new AppSearchSchema.EmbeddingPropertyConfig.Builder");
+ checkResultContains("Gift.java",
+ "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY");
+ checkResultContains("Gift.java",
+ "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE");
+ checkResultContains("Gift.java",
+ "EmbeddingVector");
+ checkEqualsGolden("Gift.java");
+ }
+
private Compilation compile(String classBody) {
return compile("Gift", classBody, /* restrictGeneratedCodeToLibrary= */false);
}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index 4d65cc9..387d97d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -2,6 +2,7 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
import java.lang.Boolean;
@@ -55,6 +56,10 @@
.addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytesProp")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vectorProp")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
.build();
}
@@ -95,6 +100,10 @@
if (bytesPropCopy != null) {
builder.setPropertyBytes("bytesProp", bytesPropCopy);
}
+ EmbeddingVector vectorPropCopy = document.vectorProp;
+ if (vectorPropCopy != null) {
+ builder.setPropertyEmbedding("vectorProp", vectorPropCopy);
+ }
return builder.build();
}
@@ -138,6 +147,11 @@
if (bytesPropCopy != null && bytesPropCopy.length != 0) {
bytesPropConv = bytesPropCopy[0];
}
+ EmbeddingVector[] vectorPropCopy = genericDoc.getPropertyEmbeddingArray("vectorProp");
+ EmbeddingVector vectorPropConv = null;
+ if (vectorPropCopy != null && vectorPropCopy.length != 0) {
+ vectorPropConv = vectorPropCopy[0];
+ }
Gift document = new Gift();
document.namespace = namespaceConv;
document.id = idConv;
@@ -148,6 +162,7 @@
document.doubleProp = doublePropConv;
document.booleanProp = booleanPropConv;
document.bytesProp = bytesPropConv;
+ document.vectorProp = vectorPropConv;
return document;
}
}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
new file mode 100644
index 0000000..9447804
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
@@ -0,0 +1,153 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+ public static final String SCHEMA_NAME = "Gift";
+
+ @Override
+ public String getSchemaName() {
+ return SCHEMA_NAME;
+ }
+
+ @Override
+ public AppSearchSchema getSchema() throws AppSearchException {
+ return new AppSearchSchema.Builder(SCHEMA_NAME)
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("name")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("defaultIndexNone")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("indexNone")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("listVec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+ .build())
+ .build();
+ }
+
+ @Override
+ public List<Class<?>> getDependencyDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+ GenericDocument.Builder<?> builder =
+ new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+ String nameCopy = document.name;
+ if (nameCopy != null) {
+ builder.setPropertyString("name", nameCopy);
+ }
+ EmbeddingVector defaultIndexNoneCopy = document.defaultIndexNone;
+ if (defaultIndexNoneCopy != null) {
+ builder.setPropertyEmbedding("defaultIndexNone", defaultIndexNoneCopy);
+ }
+ EmbeddingVector indexNoneCopy = document.indexNone;
+ if (indexNoneCopy != null) {
+ builder.setPropertyEmbedding("indexNone", indexNoneCopy);
+ }
+ EmbeddingVector vecCopy = document.vec;
+ if (vecCopy != null) {
+ builder.setPropertyEmbedding("vec", vecCopy);
+ }
+ List<EmbeddingVector> listVecCopy = document.listVec;
+ if (listVecCopy != null) {
+ EmbeddingVector[] listVecConv = listVecCopy.toArray(new EmbeddingVector[0]);
+ builder.setPropertyEmbedding("listVec", listVecConv);
+ }
+ Collection<EmbeddingVector> collectVecCopy = document.collectVec;
+ if (collectVecCopy != null) {
+ EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
+ builder.setPropertyEmbedding("collectVec", collectVecConv);
+ }
+ EmbeddingVector[] arrVecCopy = document.arrVec;
+ if (arrVecCopy != null) {
+ builder.setPropertyEmbedding("arrVec", arrVecCopy);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Gift fromGenericDocument(GenericDocument genericDoc,
+ Map<String, List<String>> documentClassMap) throws AppSearchException {
+ String namespaceConv = genericDoc.getNamespace();
+ String idConv = genericDoc.getId();
+ String[] nameCopy = genericDoc.getPropertyStringArray("name");
+ String nameConv = null;
+ if (nameCopy != null && nameCopy.length != 0) {
+ nameConv = nameCopy[0];
+ }
+ EmbeddingVector[] defaultIndexNoneCopy = genericDoc.getPropertyEmbeddingArray("defaultIndexNone");
+ EmbeddingVector defaultIndexNoneConv = null;
+ if (defaultIndexNoneCopy != null && defaultIndexNoneCopy.length != 0) {
+ defaultIndexNoneConv = defaultIndexNoneCopy[0];
+ }
+ EmbeddingVector[] indexNoneCopy = genericDoc.getPropertyEmbeddingArray("indexNone");
+ EmbeddingVector indexNoneConv = null;
+ if (indexNoneCopy != null && indexNoneCopy.length != 0) {
+ indexNoneConv = indexNoneCopy[0];
+ }
+ EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
+ EmbeddingVector vecConv = null;
+ if (vecCopy != null && vecCopy.length != 0) {
+ vecConv = vecCopy[0];
+ }
+ EmbeddingVector[] listVecCopy = genericDoc.getPropertyEmbeddingArray("listVec");
+ List<EmbeddingVector> listVecConv = null;
+ if (listVecCopy != null) {
+ listVecConv = Arrays.asList(listVecCopy);
+ }
+ EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
+ List<EmbeddingVector> collectVecConv = null;
+ if (collectVecCopy != null) {
+ collectVecConv = Arrays.asList(collectVecCopy);
+ }
+ EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
+ Gift document = new Gift();
+ document.namespace = namespaceConv;
+ document.id = idConv;
+ document.name = nameConv;
+ document.defaultIndexNone = defaultIndexNoneConv;
+ document.indexNone = indexNoneConv;
+ document.vec = vecConv;
+ document.listVec = listVecConv;
+ document.collectVec = collectVecConv;
+ document.arrVec = arrVecConv;
+ return document;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index ee36e9d6..6ac5c27 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -2,6 +2,7 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
import java.lang.Boolean;
@@ -61,6 +62,10 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setShouldIndexNestedProperties(false)
.build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
.addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrBoxLong")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setIndexingType(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE)
@@ -108,6 +113,10 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setShouldIndexNestedProperties(false)
.build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("string")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
@@ -155,6 +164,10 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setShouldIndexNestedProperties(false)
.build())
+ .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+ .build())
.build();
}
@@ -237,6 +250,11 @@
}
builder.setPropertyDocument("collectGift", collectGiftConv);
}
+ Collection<EmbeddingVector> collectVecCopy = document.collectVec;
+ if (collectVecCopy != null) {
+ EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
+ builder.setPropertyEmbedding("collectVec", collectVecConv);
+ }
Long[] arrBoxLongCopy = document.arrBoxLong;
if (arrBoxLongCopy != null) {
long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
@@ -321,6 +339,10 @@
}
builder.setPropertyDocument("arrGift", arrGiftConv);
}
+ EmbeddingVector[] arrVecCopy = document.arrVec;
+ if (arrVecCopy != null) {
+ builder.setPropertyEmbedding("arrVec", arrVecCopy);
+ }
String stringCopy = document.string;
if (stringCopy != null) {
builder.setPropertyString("string", stringCopy);
@@ -359,6 +381,10 @@
GenericDocument giftConv = GenericDocument.fromDocumentClass(giftCopy);
builder.setPropertyDocument("gift", giftConv);
}
+ EmbeddingVector vecCopy = document.vec;
+ if (vecCopy != null) {
+ builder.setPropertyEmbedding("vec", vecCopy);
+ }
return builder.build();
}
@@ -428,6 +454,11 @@
collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class, documentClassMap));
}
}
+ EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
+ List<EmbeddingVector> collectVecConv = null;
+ if (collectVecCopy != null) {
+ collectVecConv = Arrays.asList(collectVecCopy);
+ }
long[] arrBoxLongCopy = genericDoc.getPropertyLongArray("arrBoxLong");
Long[] arrBoxLongConv = null;
if (arrBoxLongCopy != null) {
@@ -497,6 +528,7 @@
arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class, documentClassMap);
}
}
+ EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
String[] stringCopy = genericDoc.getPropertyStringArray("string");
String stringConv = null;
if (stringCopy != null && stringCopy.length != 0) {
@@ -542,6 +574,11 @@
if (giftCopy != null) {
giftConv = giftCopy.toDocumentClass(Gift.class, documentClassMap);
}
+ EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
+ EmbeddingVector vecConv = null;
+ if (vecCopy != null && vecCopy.length != 0) {
+ vecConv = vecCopy[0];
+ }
Gift document = new Gift();
document.namespace = namespaceConv;
document.id = idConv;
@@ -553,6 +590,7 @@
document.collectByteArr = collectByteArrConv;
document.collectString = collectStringConv;
document.collectGift = collectGiftConv;
+ document.collectVec = collectVecConv;
document.arrBoxLong = arrBoxLongConv;
document.arrUnboxLong = arrUnboxLongConv;
document.arrBoxInteger = arrBoxIntegerConv;
@@ -566,6 +604,7 @@
document.arrUnboxByteArr = arrUnboxByteArrConv;
document.arrString = arrStringConv;
document.arrGift = arrGiftConv;
+ document.arrVec = arrVecConv;
document.string = stringConv;
document.boxLong = boxLongConv;
document.unboxLong = unboxLongConv;
@@ -579,6 +618,7 @@
document.unboxBoolean = unboxBooleanConv;
document.unboxByteArr = unboxByteArrConv;
document.gift = giftConv;
+ document.vec = vecConv;
return document;
}
}
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 8b3a414..b483b72 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -64,7 +64,8 @@
FRAMEWORK_API_TEST_ROOT = 'testing/coretests/src/android/app/appsearch/external'
FRAMEWORK_IMPL_ROOT = 'service/java/com/android/server/appsearch/external'
FRAMEWORK_IMPL_TEST_ROOT = 'testing/servicestests/src/com/android/server/appsearch/external'
-FRAMEWORK_TEST_UTIL_ROOT = '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external'
+FRAMEWORK_TEST_UTIL_ROOT = (
+ '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external')
FRAMEWORK_TEST_UTIL_TEST_ROOT = 'testing/servicestests/src/android/app/appsearch/testutil/external'
FRAMEWORK_CTS_TEST_ROOT = '../../../cts/tests/appsearch/src/com/android/cts/appsearch/external'
GOOGLE_JAVA_FORMAT = (
@@ -88,19 +89,34 @@
os.remove(abs_path)
def _TransformAndCopyFile(
- self, source_path, dest_path, transform_func=None, ignore_skips=False):
+ self, source_path, default_dest_path, transform_func=None, ignore_skips=False):
+ """
+ Transforms the file located at 'source_path' and writes it into 'default_dest_path'.
+
+ An @exportToFramework:skip() directive will skip the copy process.
+ An @exportToFramework:copyToPath() directive will override default_dest_path with another
+ path relative to framework_appsearch_root (which is usually packages/modules/AppSearch)
+ """
with open(source_path, 'r') as fh:
contents = fh.read()
if not ignore_skips and '@exportToFramework:skipFile()' in contents:
- print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
+ print('Skipping: "%s" -> "%s"' % (source_path, default_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))
+ copy_to_path = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
+ if copy_to_path:
+ dest_path = os.path.join(self._framework_appsearch_root, copy_to_path.group(1))
+ else:
+ dest_path = default_dest_path
+ self._TransformAndCopyFileToPath(source_path, dest_path, transform_func)
+
+ def _TransformAndCopyFileToPath(self, source_path, dest_path, transform_func=None):
+ """Transforms the file located at 'source_path' and writes it into 'dest_path'."""
print('Copy: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
+ with open(source_path, 'r') as fh:
+ contents = fh.read()
if transform_func:
contents = transform_func(contents)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
@@ -149,6 +165,8 @@
.replace(
'androidx.appsearch.localstorage.',
'com.android.server.appsearch.external.localstorage.')
+ .replace('androidx.appsearch.flags.FlaggedApi', 'android.annotation.FlaggedApi')
+ .replace('androidx.appsearch.flags.Flags', 'com.android.appsearch.flags.Flags')
.replace('androidx.appsearch', 'android.app.appsearch')
.replace(
'androidx.annotation.GuardedBy',
@@ -180,6 +198,7 @@
.replace('// @exportToFramework:skipFile()', '')
)
contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
+ contents = re.sub(r'@RequiresFeature\([^)]*\)', '', contents, flags=re.DOTALL)
# Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
# to allow the same documentation to compile for both.
@@ -190,16 +209,27 @@
def _TransformTestCode(self, contents):
contents = (contents
+ .replace(
+ 'androidx.appsearch.flags.CheckFlagsRule',
+ 'android.platform.test.flag.junit.CheckFlagsRule')
+ .replace(
+ 'androidx.appsearch.flags.DeviceFlagsValueProvider',
+ 'android.platform.test.flag.junit.DeviceFlagsValueProvider')
+ .replace(
+ 'androidx.appsearch.flags.RequiresFlagsEnabled',
+ 'android.platform.test.annotations.RequiresFlagsEnabled')
.replace('androidx.appsearch.testutil.', 'android.app.appsearch.testutil.')
.replace(
'package androidx.appsearch.testutil;',
'package android.app.appsearch.testutil;')
.replace(
- 'androidx.appsearch.localstorage.LocalStorage',
- 'android.app.appsearch.AppSearchManager')
+ 'import androidx.appsearch.localstorage.LocalStorage;',
+ 'import android.app.appsearch.AppSearchManager;')
.replace('LocalStorage.', 'AppSearchManager.')
)
- for shim in ['AppSearchSession', 'GlobalSearchSession', 'SearchResults']:
+ for shim in [
+ 'AppSearchSession', 'GlobalSearchSession', 'EnterpriseGlobalSearchSession',
+ 'SearchResults']:
contents = re.sub(r"([^a-zA-Z])(%s)([^a-zA-Z0-9])" % shim, r'\1\2Shim\3', contents)
return self._TransformCommonCode(contents)
@@ -277,7 +307,8 @@
self._TransformAndCopyFolder(
test_util_source_dir, test_util_dest_dir, transform_func=self._TransformTestCode)
for iface_file in (
- 'AppSearchSession.java', 'GlobalSearchSession.java', 'SearchResults.java'):
+ 'AppSearchSession.java', 'GlobalSearchSession.java',
+ 'EnterpriseGlobalSearchSession.java', 'SearchResults.java'):
dest_file_name = os.path.splitext(iface_file)[0] + 'Shim.java'
self._TransformAndCopyFile(
os.path.join(api_source_dir, 'app/' + iface_file),
@@ -318,6 +349,8 @@
'package com.android.server.appsearch.external')
.replace('com.google.android.icing.proto.',
'com.android.server.appsearch.icing.proto.')
+ .replace('com.google.android.appsearch.proto.',
+ 'com.android.server.appsearch.appsearch.proto.')
.replace('com.google.android.icing.protobuf.',
'com.android.server.appsearch.protobuf.')
)
@@ -360,6 +393,32 @@
print('Wrote "%s"' % file_path)
return old_sha
+ def FormatCommitMessage(self, old_sha, new_sha):
+ print('\nCommand to diff old version to new version:')
+ print(' git log --pretty=format:"* %h %s" {}..{} -- appsearch/'.format(old_sha, new_sha))
+ pretty_log = subprocess.check_output([
+ 'git',
+ 'log',
+ '--pretty=format:* %h %s',
+ '{}..{}'.format(old_sha, new_sha),
+ '--',
+ 'appsearch/'
+ ]).decode("utf-8")
+ bug_output = subprocess.check_output([
+ '/bin/sh',
+ '-c',
+ 'git log {}..{} -- appsearch/ | grep Bug: | sort | uniq'.format(old_sha, new_sha)
+ ]).decode("utf-8")
+
+ print('\n--------------------------------------------------')
+ print('Update Framework from Jetpack.\n')
+ print(pretty_log)
+ print()
+ for line in bug_output.splitlines():
+ print(line.strip())
+ print('Test: Presubmit\n')
+ print('--------------------------------------------------\n')
+
if __name__ == '__main__':
if len(sys.argv) != 3:
@@ -388,5 +447,4 @@
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))
+ exporter.FormatCommitMessage(old_sha, new_sha)
diff --git a/benchmark/benchmark-common/api/1.3.0-beta01.txt b/benchmark/benchmark-common/api/1.3.0-beta01.txt
deleted file mode 100644
index 9df6fae..0000000
--- a/benchmark/benchmark-common/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,139 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark {
-
- public final class BenchmarkState {
- ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
- method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
- method public boolean keepRunning();
- method public void pauseTiming();
- method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
- method public void resumeTiming();
- field public static final androidx.benchmark.BenchmarkState.Companion Companion;
- }
-
- public static final class BenchmarkState.Companion {
- method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
- method public static void consume(boolean value);
- method public static void consume(byte value);
- method public static void consume(char value);
- method public static void consume(double value);
- method public static void consume(float value);
- method public static void consume(int value);
- method public static void consume(Object value);
- method public static void consume(long value);
- method public static void consume(short value);
- field public static final androidx.benchmark.BlackHole INSTANCE;
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkConfigApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBlackHoleApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract class MetricCapture {
- ctor public MetricCapture(java.util.List<java.lang.String> names);
- method public abstract void capturePaused();
- method public abstract void captureResumed();
- method public abstract void captureStart(long timeNs);
- method public abstract void captureStop(long timeNs, long[] output, int offset);
- method public final java.util.List<java.lang.String> getNames();
- property public final java.util.List<java.lang.String> names;
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
- ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
- method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
- method public androidx.benchmark.ProfilerConfig? getProfiler();
- method public boolean isPerfettoSdkTracingEnabled();
- method public boolean isTraceAppTagEnabled();
- property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
- property public final boolean perfettoSdkTracingEnabled;
- property public final androidx.benchmark.ProfilerConfig? profiler;
- property public final boolean traceAppTagEnabled;
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
- }
-
- public static final class ProfilerConfig.MethodTracing extends androidx.benchmark.ProfilerConfig {
- ctor public ProfilerConfig.MethodTracing();
- field public static final boolean AFFECTS_MEASUREMENTS_ON_THIS_DEVICE;
- field public static final androidx.benchmark.ProfilerConfig.MethodTracing.Companion Companion;
- }
-
- public static final class ProfilerConfig.MethodTracing.Companion {
- }
-
- public static final class ProfilerConfig.StackSampling extends androidx.benchmark.ProfilerConfig {
- ctor public ProfilerConfig.StackSampling();
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class TimeCapture extends androidx.benchmark.MetricCapture {
- ctor public TimeCapture();
- ctor public TimeCapture(optional String name);
- method public void capturePaused();
- method public void captureResumed();
- method public void captureStart(long timeNs);
- method public void captureStop(long timeNs, long[] output, int offset);
- }
-
-}
-
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public abstract sealed class PerfettoConfig {
- }
-
- public static final class PerfettoConfig.Binary extends androidx.benchmark.perfetto.PerfettoConfig {
- ctor public PerfettoConfig.Binary(byte[] bytes);
- method public byte[] getBytes();
- property public final byte[] bytes;
- }
-
- public static final class PerfettoConfig.Text extends androidx.benchmark.perfetto.PerfettoConfig {
- ctor public PerfettoConfig.Text(String text);
- method public String getText();
- property public final String text;
- }
-
- @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
- ctor public PerfettoTrace(String path);
- method public String getPath();
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- property public final String path;
- field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
- }
-
- public static final class PerfettoTrace.Companion {
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- }
-
-}
-
diff --git a/benchmark/benchmark-common/api/res-1.3.0-beta01.txt b/benchmark/benchmark-common/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-common/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e2bfb40..0000000
--- a/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,141 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark {
-
- public final class BenchmarkState {
- ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
- method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
- method public boolean keepRunning();
- method @kotlin.PublishedApi internal boolean keepRunningInternal();
- method public void pauseTiming();
- method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
- method public void resumeTiming();
- field public static final androidx.benchmark.BenchmarkState.Companion Companion;
- field @kotlin.PublishedApi internal int iterationsRemaining;
- }
-
- public static final class BenchmarkState.Companion {
- method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
- method public static void consume(boolean value);
- method public static void consume(byte value);
- method public static void consume(char value);
- method public static void consume(double value);
- method public static void consume(float value);
- method public static void consume(int value);
- method public static void consume(Object value);
- method public static void consume(long value);
- method public static void consume(short value);
- field public static final androidx.benchmark.BlackHole INSTANCE;
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkConfigApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBlackHoleApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract class MetricCapture {
- ctor public MetricCapture(java.util.List<java.lang.String> names);
- method public abstract void capturePaused();
- method public abstract void captureResumed();
- method public abstract void captureStart(long timeNs);
- method public abstract void captureStop(long timeNs, long[] output, int offset);
- method public final java.util.List<java.lang.String> getNames();
- property public final java.util.List<java.lang.String> names;
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
- ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
- method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
- method public androidx.benchmark.ProfilerConfig? getProfiler();
- method public boolean isPerfettoSdkTracingEnabled();
- method public boolean isTraceAppTagEnabled();
- property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
- property public final boolean perfettoSdkTracingEnabled;
- property public final androidx.benchmark.ProfilerConfig? profiler;
- property public final boolean traceAppTagEnabled;
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
- }
-
- public static final class ProfilerConfig.MethodTracing extends androidx.benchmark.ProfilerConfig {
- ctor public ProfilerConfig.MethodTracing();
- field public static final boolean AFFECTS_MEASUREMENTS_ON_THIS_DEVICE;
- field public static final androidx.benchmark.ProfilerConfig.MethodTracing.Companion Companion;
- }
-
- public static final class ProfilerConfig.MethodTracing.Companion {
- }
-
- public static final class ProfilerConfig.StackSampling extends androidx.benchmark.ProfilerConfig {
- ctor public ProfilerConfig.StackSampling();
- }
-
- @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class TimeCapture extends androidx.benchmark.MetricCapture {
- ctor public TimeCapture();
- ctor public TimeCapture(optional String name);
- method public void capturePaused();
- method public void captureResumed();
- method public void captureStart(long timeNs);
- method public void captureStop(long timeNs, long[] output, int offset);
- }
-
-}
-
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public abstract sealed class PerfettoConfig {
- }
-
- public static final class PerfettoConfig.Binary extends androidx.benchmark.perfetto.PerfettoConfig {
- ctor public PerfettoConfig.Binary(byte[] bytes);
- method public byte[] getBytes();
- property public final byte[] bytes;
- }
-
- public static final class PerfettoConfig.Text extends androidx.benchmark.perfetto.PerfettoConfig {
- ctor public PerfettoConfig.Text(String text);
- method public String getText();
- property public final String text;
- }
-
- @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
- ctor public PerfettoTrace(String path);
- method public String getPath();
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- property public final String path;
- field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
- }
-
- public static final class PerfettoTrace.Companion {
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- }
-
-}
-
diff --git a/benchmark/benchmark-junit4/api/1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/1.3.0-beta01.txt
deleted file mode 100644
index fc4b392..0000000
--- a/benchmark/benchmark-junit4/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,36 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.junit4 {
-
- public class AndroidBenchmarkRunner extends androidx.test.runner.AndroidJUnitRunner {
- ctor public AndroidBenchmarkRunner();
- }
-
- public final class BenchmarkRule implements org.junit.rules.TestRule {
- ctor public BenchmarkRule();
- ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public androidx.benchmark.BenchmarkState getState();
- }
-
- public final class BenchmarkRule.Scope {
- method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
- }
-
- public final class BenchmarkRuleKt {
- method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
- method public static inline void measureRepeatedOnMainThread(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
- ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public boolean getEnableAppTagTracing();
- method public boolean getEnableUserspaceTracing();
- method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
- property public final boolean enableAppTagTracing;
- property public final boolean enableUserspaceTracing;
- property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
- }
-
-}
-
diff --git a/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index 6aac49d..0000000
--- a/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.junit4 {
-
- public class AndroidBenchmarkRunner extends androidx.test.runner.AndroidJUnitRunner {
- ctor public AndroidBenchmarkRunner();
- }
-
- public final class BenchmarkRule implements org.junit.rules.TestRule {
- ctor public BenchmarkRule();
- ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public androidx.benchmark.BenchmarkState getState();
- }
-
- public final class BenchmarkRule.Scope {
- method @kotlin.PublishedApi internal androidx.benchmark.BenchmarkState getOuterState();
- method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
- }
-
- public final class BenchmarkRuleKt {
- method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
- method public static inline void measureRepeatedOnMainThread(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
- ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public boolean getEnableAppTagTracing();
- method public boolean getEnableUserspaceTracing();
- method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
- property public final boolean enableAppTagTracing;
- property public final boolean enableUserspaceTracing;
- property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
- }
-
-}
-
diff --git a/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt
deleted file mode 100644
index e06bf57..0000000
--- a/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro.junit4 {
-
- @RequiresApi(28) public final class BaselineProfileRule implements org.junit.rules.TestRule {
- ctor public BaselineProfileRule();
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, optional kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> filterPredicate, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- }
-
- public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
- ctor public MacrobenchmarkRule();
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- }
-
-}
-
diff --git a/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e06bf57..0000000
--- a/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro.junit4 {
-
- @RequiresApi(28) public final class BaselineProfileRule implements org.junit.rules.TestRule {
- ctor public BaselineProfileRule();
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, optional kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> filterPredicate, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, optional int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- method public void collect(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
- }
-
- public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
- ctor public MacrobenchmarkRule();
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
- }
-
-}
-
diff --git a/benchmark/benchmark-macro/api/1.3.0-beta01.txt b/benchmark/benchmark-macro/api/1.3.0-beta01.txt
deleted file mode 100644
index ad9fb40..0000000
--- a/benchmark/benchmark-macro/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,281 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro {
-
- public enum BaselineProfileMode {
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
- }
-
- public abstract sealed class CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
- field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
- }
-
- public static final class CompilationMode.Companion {
- }
-
- public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Full();
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMacrobenchmarkApi public static final class CompilationMode.Ignore extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Ignore();
- }
-
- @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.None();
- }
-
- @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Partial();
- ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
- ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0L) int warmupIterations);
- method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
- method public int getWarmupIterations();
- property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
- property public final int warmupIterations;
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn(message="This Macrobenchmark API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMacrobenchmarkApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
- ctor public FrameTimingGfxInfoMetric();
- }
-
- public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
- ctor public FrameTimingMetric();
- }
-
- public final class MacrobenchmarkScope {
- ctor public MacrobenchmarkScope(String packageName, boolean launchWithClearTask);
- method public void dropKernelPageCache();
- method public void dropShaderCache();
- method public androidx.test.uiautomator.UiDevice getDevice();
- method public Integer? getIteration();
- method public String getPackageName();
- method public void killProcess();
- method @Deprecated public void killProcess(optional boolean useKillAll);
- method public void pressHome();
- method public void pressHome(optional long delayDurationMs);
- method public void startActivityAndWait();
- method public void startActivityAndWait(android.content.Intent intent);
- method public void startActivityAndWait(optional kotlin.jvm.functions.Function1<? super android.content.Intent,kotlin.Unit> block);
- property public final androidx.test.uiautomator.UiDevice device;
- property public final Integer? iteration;
- property public final String packageName;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
- ctor public MemoryCountersMetric();
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
- ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- public enum MemoryUsageMetric.Mode {
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Last;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Max;
- }
-
- public enum MemoryUsageMetric.SubMetric {
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric Gpu;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric HeapSize;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssAnon;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssFile;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssShmem;
- }
-
- public abstract sealed class Metric {
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.CaptureInfo {
- ctor public Metric.CaptureInfo(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
- method public int component1();
- method public String component2();
- method public String component3();
- method public androidx.benchmark.macro.StartupMode? component4();
- method public androidx.benchmark.macro.Metric.CaptureInfo copy(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
- method public int getApiLevel();
- method public androidx.benchmark.macro.StartupMode? getStartupMode();
- method public String getTargetPackageName();
- method public String getTestPackageName();
- property public final int apiLevel;
- property public final androidx.benchmark.macro.StartupMode? startupMode;
- property public final String targetPackageName;
- property public final String testPackageName;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.Measurement {
- ctor public Metric.Measurement(String name, double data);
- ctor public Metric.Measurement(String name, java.util.List<java.lang.Double> dataSamples);
- method public String component1();
- method public java.util.List<java.lang.Double> component2();
- method public boolean component3();
- method public androidx.benchmark.macro.Metric.Measurement copy(String name, java.util.List<java.lang.Double> data, boolean requireSingleValue);
- method public java.util.List<java.lang.Double> getData();
- method public String getName();
- method public boolean getRequireSingleValue();
- property public final java.util.List<java.lang.Double> data;
- property public final String name;
- property public final boolean requireSingleValue;
- }
-
- public final class MetricResultExtensionsKt {
- method @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static void assertEqualMeasurements(java.util.List<androidx.benchmark.macro.Metric.Measurement> expected, java.util.List<androidx.benchmark.macro.Metric.Measurement> observed, double threshold);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategory {
- enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
- enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
- enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
- enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
- enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
- enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
- enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
- enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategoryDisplayLevel {
- enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
- enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
- }
-
- @SuppressCompatibility @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class PowerMetric extends androidx.benchmark.macro.Metric {
- ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
- method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
- method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public static boolean deviceBatteryHasMinimumCharge();
- method public static boolean deviceSupportsHighPrecisionTracking();
- field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
- }
-
- public static final class PowerMetric.Companion {
- method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
- method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public boolean deviceBatteryHasMinimumCharge();
- method public boolean deviceSupportsHighPrecisionTracking();
- }
-
- public abstract static sealed class PowerMetric.Type {
- method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
- method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
- property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
- }
-
- public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Battery();
- }
-
- public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
- }
-
- public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
- }
-
- public enum StartupMode {
- enum_constant public static final androidx.benchmark.macro.StartupMode COLD;
- enum_constant public static final androidx.benchmark.macro.StartupMode HOT;
- enum_constant public static final androidx.benchmark.macro.StartupMode WARM;
- }
-
- public final class StartupTimingMetric extends androidx.benchmark.macro.Metric {
- ctor public StartupTimingMetric();
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
- ctor public TraceMetric();
- method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
- ctor public TraceSectionMetric(String sectionName);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
- }
-
- public abstract static sealed class TraceSectionMetric.Mode {
- }
-
- public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
- }
-
-}
-
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
- ctor public PerfettoTraceProcessor();
- method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
- method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
- }
-
- public static final class PerfettoTraceProcessor.Companion {
- method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- }
-
- public static final class PerfettoTraceProcessor.Session {
- method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
- method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
- method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
- method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
- method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
- ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
- method public byte[] bytes(String columnName);
- method public double double(String columnName);
- method public long long(String columnName);
- method public byte[]? nullableBytes(String columnName);
- method public Double? nullableDouble(String columnName);
- method public Long? nullableLong(String columnName);
- method public String? nullableString(String columnName);
- method public String string(String columnName);
- }
-
- public final class RowKt {
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
- }
-
-}
-
diff --git a/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt b/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index 4c883fb..0000000
--- a/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,303 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro {
-
- public enum BaselineProfileMode {
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
- enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
- }
-
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BatteryCharge {
- method public boolean hasMinimumCharge(optional boolean throwOnMissingMetrics);
- field public static final androidx.benchmark.macro.BatteryCharge INSTANCE;
- }
-
- public abstract sealed class CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
- field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
- }
-
- public static final class CompilationMode.Companion {
- }
-
- public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Full();
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMacrobenchmarkApi public static final class CompilationMode.Ignore extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Ignore();
- }
-
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class CompilationMode.Interpreted extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Interpreted INSTANCE;
- }
-
- @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.None();
- }
-
- @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.Partial();
- ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
- ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0L) int warmupIterations);
- method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
- method public int getWarmupIterations();
- property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
- property public final int warmupIterations;
- }
-
- public final class CompilationModeKt {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static boolean isSupportedWithVmSettings(androidx.benchmark.macro.CompilationMode);
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn(message="This Macrobenchmark API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMacrobenchmarkApi {
- }
-
- @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
- ctor public FrameTimingGfxInfoMetric();
- }
-
- public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
- ctor public FrameTimingMetric();
- }
-
- public final class MacrobenchmarkScope {
- ctor public MacrobenchmarkScope(String packageName, boolean launchWithClearTask);
- method public void dropKernelPageCache();
- method public void dropShaderCache();
- method public androidx.test.uiautomator.UiDevice getDevice();
- method public Integer? getIteration();
- method public String getPackageName();
- method public void killProcess();
- method @Deprecated public void killProcess(optional boolean useKillAll);
- method public void pressHome();
- method public void pressHome(optional long delayDurationMs);
- method public void startActivityAndWait();
- method public void startActivityAndWait(android.content.Intent intent);
- method public void startActivityAndWait(optional kotlin.jvm.functions.Function1<? super android.content.Intent,kotlin.Unit> block);
- property public final androidx.test.uiautomator.UiDevice device;
- property public final Integer? iteration;
- property public final String packageName;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
- ctor public MemoryCountersMetric();
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
- ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- public enum MemoryUsageMetric.Mode {
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Last;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Max;
- }
-
- public enum MemoryUsageMetric.SubMetric {
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric Gpu;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric HeapSize;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssAnon;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssFile;
- enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssShmem;
- }
-
- public abstract sealed class Metric {
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.CaptureInfo {
- ctor public Metric.CaptureInfo(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
- method public int component1();
- method public String component2();
- method public String component3();
- method public androidx.benchmark.macro.StartupMode? component4();
- method public androidx.benchmark.macro.Metric.CaptureInfo copy(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
- method public int getApiLevel();
- method public androidx.benchmark.macro.StartupMode? getStartupMode();
- method public String getTargetPackageName();
- method public String getTestPackageName();
- property public final int apiLevel;
- property public final androidx.benchmark.macro.StartupMode? startupMode;
- property public final String targetPackageName;
- property public final String testPackageName;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.Measurement {
- ctor public Metric.Measurement(String name, double data);
- ctor public Metric.Measurement(String name, java.util.List<java.lang.Double> dataSamples);
- method public String component1();
- method public java.util.List<java.lang.Double> component2();
- method public boolean component3();
- method public androidx.benchmark.macro.Metric.Measurement copy(String name, java.util.List<java.lang.Double> data, boolean requireSingleValue);
- method public java.util.List<java.lang.Double> getData();
- method public String getName();
- method public boolean getRequireSingleValue();
- property public final java.util.List<java.lang.Double> data;
- property public final String name;
- property public final boolean requireSingleValue;
- }
-
- public final class MetricResultExtensionsKt {
- method @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static void assertEqualMeasurements(java.util.List<androidx.benchmark.macro.Metric.Measurement> expected, java.util.List<androidx.benchmark.macro.Metric.Measurement> observed, double threshold);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategory {
- enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
- enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
- enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
- enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
- enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
- enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
- enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
- enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategoryDisplayLevel {
- enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
- enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
- }
-
- @SuppressCompatibility @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class PowerMetric extends androidx.benchmark.macro.Metric {
- ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
- method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
- method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public static boolean deviceBatteryHasMinimumCharge();
- method public static boolean deviceSupportsHighPrecisionTracking();
- field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
- }
-
- public static final class PowerMetric.Companion {
- method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
- method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
- method public boolean deviceBatteryHasMinimumCharge();
- method public boolean deviceSupportsHighPrecisionTracking();
- }
-
- public abstract static sealed class PowerMetric.Type {
- method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
- method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
- property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
- }
-
- public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Battery();
- }
-
- public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
- }
-
- public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
- ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
- }
-
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PowerRail {
- method public boolean hasMetrics(optional boolean throwOnMissingMetrics);
- field public static final androidx.benchmark.macro.PowerRail INSTANCE;
- }
-
- public enum StartupMode {
- enum_constant public static final androidx.benchmark.macro.StartupMode COLD;
- enum_constant public static final androidx.benchmark.macro.StartupMode HOT;
- enum_constant public static final androidx.benchmark.macro.StartupMode WARM;
- }
-
- @RequiresApi(29) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class StartupTimingLegacyMetric extends androidx.benchmark.macro.Metric {
- ctor public StartupTimingLegacyMetric();
- }
-
- public final class StartupTimingMetric extends androidx.benchmark.macro.Metric {
- ctor public StartupTimingMetric();
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
- ctor public TraceMetric();
- method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
- }
-
- @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
- ctor public TraceSectionMetric(String sectionName);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label);
- ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
- }
-
- public abstract static sealed class TraceSectionMetric.Mode {
- }
-
- public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
- }
-
- public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
- field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
- }
-
-}
-
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
- ctor public PerfettoTraceProcessor();
- method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
- method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
- }
-
- public static final class PerfettoTraceProcessor.Companion {
- method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- }
-
- public static final class PerfettoTraceProcessor.Session {
- method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
- method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
- method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
- method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
- method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
- ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
- method public byte[] bytes(String columnName);
- method public double double(String columnName);
- method public long long(String columnName);
- method public byte[]? nullableBytes(String columnName);
- method public Double? nullableDouble(String columnName);
- method public Long? nullableLong(String columnName);
- method public String? nullableString(String columnName);
- method public String string(String columnName);
- }
-
- public final class RowKt {
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
- }
-
-}
-
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index fbe028a..f038350 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -28,6 +28,7 @@
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
}
}
+ compileSdkVersion 35
namespace "androidx.benchmark.integration.macrobenchmark.target"
}
diff --git a/biometric/biometric-ktx/build.gradle b/biometric/biometric-ktx/build.gradle
index 0a615c2..740992b 100644
--- a/biometric/biometric-ktx/build.gradle
+++ b/biometric/biometric-ktx/build.gradle
@@ -47,5 +47,6 @@
}
android {
+ compileSdk 35
namespace "androidx.biometric.ktx"
}
diff --git a/biometric/biometric-ktx/samples/build.gradle b/biometric/biometric-ktx/samples/build.gradle
index 0e63ec0..c750a94 100644
--- a/biometric/biometric-ktx/samples/build.gradle
+++ b/biometric/biometric-ktx/samples/build.gradle
@@ -42,5 +42,6 @@
}
android {
+ compileSdk 35
namespace "androidx.biometric.samples"
}
diff --git a/biometric/biometric/api/current.txt b/biometric/biometric/api/current.txt
index 6a60ed1..eb18aba 100644
--- a/biometric/biometric/api/current.txt
+++ b/biometric/biometric/api/current.txt
@@ -43,6 +43,7 @@
field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
field public static final int ERROR_LOCKOUT = 7; // 0x7
field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
@@ -72,16 +73,22 @@
ctor public BiometricPrompt.CryptoObject(java.security.Signature);
ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
method public javax.crypto.Cipher? getCipher();
method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
method public javax.crypto.Mac? getMac();
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
method public java.security.Signature? getSignature();
}
public static class BiometricPrompt.PromptInfo {
method public int getAllowedAuthenticators();
+ method public androidx.biometric.PromptContentView? getContentView();
method public CharSequence? getDescription();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
+ method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
method public CharSequence getNegativeButtonText();
method public CharSequence? getSubtitle();
method public CharSequence getTitle();
@@ -94,13 +101,54 @@
method public androidx.biometric.BiometricPrompt.PromptInfo build();
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
}
+ public interface PromptContentItem {
+ }
+
+ public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
+ ctor public PromptContentItemBulletedText(String);
+ }
+
+ public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
+ ctor public PromptContentItemPlainText(String);
+ }
+
+ public interface PromptContentView {
+ }
+
+ public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
+ }
+
+ public static final class PromptContentViewWithMoreOptionsButton.Builder {
+ ctor public PromptContentViewWithMoreOptionsButton.Builder();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
+ }
+
+ public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
+ method public String? getDescription();
+ method public java.util.List<androidx.biometric.PromptContentItem!> getListItems();
+ }
+
+ public static final class PromptVerticalListContentView.Builder {
+ ctor public PromptVerticalListContentView.Builder();
+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
+ method public androidx.biometric.PromptVerticalListContentView build();
+ method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
+ }
+
}
package androidx.biometric.auth {
diff --git a/biometric/biometric/api/restricted_current.txt b/biometric/biometric/api/restricted_current.txt
index 6a60ed1..eb18aba 100644
--- a/biometric/biometric/api/restricted_current.txt
+++ b/biometric/biometric/api/restricted_current.txt
@@ -43,6 +43,7 @@
field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
field public static final int ERROR_LOCKOUT = 7; // 0x7
field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
@@ -72,16 +73,22 @@
ctor public BiometricPrompt.CryptoObject(java.security.Signature);
ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
method public javax.crypto.Cipher? getCipher();
method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
method public javax.crypto.Mac? getMac();
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
method public java.security.Signature? getSignature();
}
public static class BiometricPrompt.PromptInfo {
method public int getAllowedAuthenticators();
+ method public androidx.biometric.PromptContentView? getContentView();
method public CharSequence? getDescription();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
+ method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
method public CharSequence getNegativeButtonText();
method public CharSequence? getSubtitle();
method public CharSequence getTitle();
@@ -94,13 +101,54 @@
method public androidx.biometric.BiometricPrompt.PromptInfo build();
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
}
+ public interface PromptContentItem {
+ }
+
+ public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
+ ctor public PromptContentItemBulletedText(String);
+ }
+
+ public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
+ ctor public PromptContentItemPlainText(String);
+ }
+
+ public interface PromptContentView {
+ }
+
+ public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
+ }
+
+ public static final class PromptContentViewWithMoreOptionsButton.Builder {
+ ctor public PromptContentViewWithMoreOptionsButton.Builder();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
+ }
+
+ public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
+ method public String? getDescription();
+ method public java.util.List<androidx.biometric.PromptContentItem!> getListItems();
+ }
+
+ public static final class PromptVerticalListContentView.Builder {
+ ctor public PromptVerticalListContentView.Builder();
+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
+ method public androidx.biometric.PromptVerticalListContentView build();
+ method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
+ }
+
}
package androidx.biometric.auth {
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index c7b5373..61506c5 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -28,6 +28,10 @@
id("com.android.library")
}
+android {
+ compileSdk = 35
+}
+
dependencies {
// Public API dependencies
api("androidx.annotation:annotation:1.6.0")
@@ -69,6 +73,7 @@
}
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.biometric"
+ compileSdk = 35
}
androidx {
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
index c29b0bb..36b6817 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
@@ -16,11 +16,13 @@
package androidx.biometric;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -81,13 +83,20 @@
static final int CANCELED_FROM_CLIENT = 3;
/**
+ * Authentication was canceled by the user by pressing the more options button on the prompt
+ * content.
+ */
+ static final int CANCELED_FROM_MORE_OPTIONS_BUTTON = 4;
+
+ /**
* Where authentication was canceled from.
*/
@IntDef({
CANCELED_FROM_INTERNAL,
CANCELED_FROM_USER,
CANCELED_FROM_NEGATIVE_BUTTON,
- CANCELED_FROM_CLIENT
+ CANCELED_FROM_CLIENT,
+ CANCELED_FROM_MORE_OPTIONS_BUTTON
})
@Retention(RetentionPolicy.SOURCE)
@interface CanceledFrom {}
@@ -352,6 +361,15 @@
}
});
+ mViewModel.isMoreOptionsButtonPressPending().observe(this,
+ moreOptionsButtonPressPending -> {
+ if (moreOptionsButtonPressPending) {
+ onMoreOptionsButtonPressed();
+ mViewModel.setMoreOptionsButtonPressPending(false);
+ }
+ }
+ );
+
mViewModel.isFingerprintDialogCancelPending().observe(this,
fingerprintDialogCancelPending -> {
if (fingerprintDialogCancelPending) {
@@ -514,6 +532,29 @@
builder, AuthenticatorUtils.isDeviceCredentialAllowed(authenticators));
}
+ // Set the custom biometric prompt features introduced in Android 15 (API 35).
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ final int logoRes = mViewModel.getLogoRes();
+ final Bitmap logoBitmap = mViewModel.getLogoBitmap();
+ final String logoDescription = mViewModel.getLogoDescription();
+ final android.hardware.biometrics.PromptContentView contentView =
+ PromptContentViewUtils.wrapForBiometricPrompt(mViewModel.getContentView(),
+ mViewModel.getClientExecutor(),
+ mViewModel.getMoreOptionsButtonListener());
+ if (logoRes != -1) {
+ Api35Impl.setLogoRes(builder, logoRes);
+ }
+ if (logoBitmap != null) {
+ Api35Impl.setLogoBitmap(builder, logoBitmap);
+ }
+ if (logoDescription != null && !logoDescription.isEmpty()) {
+ Api35Impl.setLogoDescription(builder, logoDescription);
+ }
+ if (contentView != null) {
+ Api35Impl.setContentView(builder, contentView);
+ }
+ }
+
authenticateWithBiometricPrompt(Api28Impl.buildPrompt(builder), getContext());
}
@@ -779,6 +820,16 @@
}
/**
+ * Callback that is run when the view model reports that the more options button has been
+ * pressed on the prompt content.
+ */
+ void onMoreOptionsButtonPressed() {
+ sendErrorAndDismiss(BiometricPrompt.ERROR_MORE_OPTIONS_BUTTON,
+ "More options button in the content view is clicked.");
+ cancelAuthentication(BiometricFragment.CANCELED_FROM_MORE_OPTIONS_BUTTON);
+ }
+
+ /**
* Launches the confirm device credential Settings activity, where the user can authenticate
* using their PIN, pattern, or password.
*/
@@ -1097,6 +1148,72 @@
}
/**
+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @SuppressLint("MissingPermission")
+ private static class Api35Impl {
+ // Prevent instantiation.
+ private Api35Impl() {
+ }
+
+ /**
+ * Sets the prompt content view for the given framework prompt builder.
+ *
+ * @param builder An instance of
+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+ * @param logoRes A drawable resource of the logo that will be shown on the prompt.
+ */
+ @DoNotInline
+ static void setLogoRes(
+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder, int logoRes) {
+ builder.setLogoRes(logoRes);
+ }
+
+ /**
+ * Sets the prompt content view for the given framework prompt builder.
+ *
+ * @param builder An instance of
+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+ * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
+ */
+ @DoNotInline
+ static void setLogoBitmap(
+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+ @NonNull Bitmap logoBitmap) {
+ builder.setLogoBitmap(logoBitmap);
+ }
+
+ /**
+ * Sets the prompt content view for the given framework prompt builder.
+ *
+ * @param builder An instance of
+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+ * @param logoDescription The content view for the prompt.
+ */
+ @DoNotInline
+ static void setLogoDescription(
+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+ String logoDescription) {
+ builder.setLogoDescription(logoDescription);
+ }
+
+ /**
+ * Sets the prompt content view for the given framework prompt builder.
+ *
+ * @param builder An instance of
+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+ * @param contentView The content view for the prompt.
+ */
+ @DoNotInline
+ static void setContentView(
+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+ @NonNull android.hardware.biometrics.PromptContentView contentView) {
+ builder.setContentView(contentView);
+ }
+ }
+
+ /**
* Nested class to avoid verification errors for methods introduced in Android 11 (API 30).
*/
@RequiresApi(Build.VERSION_CODES.R)
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
index ed361eb..01ff4b7 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
@@ -16,15 +16,20 @@
package androidx.biometric;
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
+
import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.biometric.BiometricManager.Authenticators;
@@ -160,6 +165,11 @@
public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15;
/**
+ * The user pressed the more options button on prompt content.
+ */
+ public static final int ERROR_MORE_OPTIONS_BUTTON = 16;
+
+ /**
* An error code that may be returned during authentication.
*/
@IntDef({
@@ -175,7 +185,8 @@
ERROR_NO_BIOMETRICS,
ERROR_HW_NOT_PRESENT,
ERROR_NEGATIVE_BUTTON,
- ERROR_NO_DEVICE_CREDENTIAL
+ ERROR_NO_DEVICE_CREDENTIAL,
+ ERROR_MORE_OPTIONS_BUTTON
})
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@@ -230,6 +241,7 @@
@Nullable private final Mac mMac;
@Nullable private final android.security.identity.IdentityCredential mIdentityCredential;
@Nullable private final android.security.identity.PresentationSession mPresentationSession;
+ private final long mOperationHandle;
/**
* Creates a crypto object that wraps the given signature object.
@@ -242,6 +254,7 @@
mMac = null;
mIdentityCredential = null;
mPresentationSession = null;
+ mOperationHandle = 0;
}
/**
@@ -255,6 +268,7 @@
mMac = null;
mIdentityCredential = null;
mPresentationSession = null;
+ mOperationHandle = 0;
}
/**
@@ -268,6 +282,7 @@
mMac = mac;
mIdentityCredential = null;
mPresentationSession = null;
+ mOperationHandle = 0;
}
/**
@@ -284,6 +299,7 @@
mMac = null;
mIdentityCredential = identityCredential;
mPresentationSession = null;
+ mOperationHandle = 0;
}
/**
@@ -300,9 +316,27 @@
mMac = null;
mIdentityCredential = null;
mPresentationSession = presentationSession;
+ mOperationHandle = 0;
}
/**
+ * Create from an operation handle.
+ * @see CryptoObject#getOperationHandle()
+ *
+ * @param operationHandle the operation handle associated with this object.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public CryptoObject(long operationHandle) {
+ mSignature = null;
+ mCipher = null;
+ mMac = null;
+ mIdentityCredential = null;
+ mPresentationSession = null;
+ mOperationHandle = operationHandle;
+ }
+
+
+ /**
* Gets the signature object associated with this crypto object.
*
* @return The signature, or {@code null} if none is associated with this object.
@@ -353,6 +387,34 @@
public android.security.identity.PresentationSession getPresentationSession() {
return mPresentationSession;
}
+
+ /**
+ * Returns the {@code operationHandle} associated with this object or 0 if none.
+ * The {@code operationHandle} is the underlying identifier associated with
+ * the {@code CryptoObject}.
+ *
+ * <p> The {@code operationHandle} can be used to reconstruct a {@code CryptoObject}
+ * instance. This is useful for any cross-process communication as the {@code CryptoObject}
+ * class is not {@link android.os.Parcelable}. Hence, if the {@code CryptoObject} is
+ * constructed in one process, and needs to be propagated to another process,
+ * before calling the {@code authenticate()} API in the second process, the
+ * recommendation is to retrieve the {@code operationHandle} using this API, and then
+ * reconstruct the {@code CryptoObject}using the constructor that takes in an {@code
+ * operationHandle}, and pass that in to the {@code authenticate} API mentioned above.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public long getOperationHandle() {
+ return CryptoObjectUtils.getOperationHandle(this);
+ }
+
+ /**
+ * Returns the {@code operationHandle} from the constructor. This is only for wrapping
+ * this {@link androidx.biometric.BiometricPrompt.CryptoObject} to
+ * {@link android.hardware.biometrics.BiometricPrompt}.
+ */
+ long getOperationHandleCryptoObject() {
+ return mOperationHandle;
+ }
}
/**
@@ -438,15 +500,75 @@
*/
public static class Builder {
// Mutable options to be set on the builder.
+ @DrawableRes private int mLogoRes = -1;
+ @Nullable private Bitmap mLogoBitmap = null;
+ @Nullable private String mLogoDescription = null;
@Nullable private CharSequence mTitle = null;
@Nullable private CharSequence mSubtitle = null;
@Nullable private CharSequence mDescription = null;
+ @Nullable private PromptContentView mPromptContentView = null;
@Nullable private CharSequence mNegativeButtonText = null;
private boolean mIsConfirmationRequired = true;
private boolean mIsDeviceCredentialAllowed = false;
@BiometricManager.AuthenticatorTypes private int mAllowedAuthenticators = 0;
/**
+ * Optional: Sets the drawable resource of the logo that will be shown on the prompt.
+ *
+ * <p> Note that using this method is not recommended in most scenarios because the
+ * calling application's icon will be used by default. Setting the logo is intended
+ * for large bundled applications that perform a wide range of functions and need to
+ * show distinct icons for each function.
+ *
+ * @param logoRes A drawable resource of the logo that will be shown on the prompt.
+ * @return This builder.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @NonNull
+ public Builder setLogoRes(@DrawableRes int logoRes) {
+ mLogoRes = logoRes;
+ return this;
+ }
+
+ /**
+ * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt.
+ *
+ * <p> Note that using this method is not recommended in most scenarios because the
+ * calling application's icon will be used by default. Setting the logo is intended
+ * for large bundled applications that perform a wide range of functions and need to
+ * show distinct icons for each function.
+ *
+ * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
+ * @return This builder.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @NonNull
+ public Builder setLogoBitmap(@NonNull Bitmap logoBitmap) {
+ mLogoBitmap = logoBitmap;
+ return this;
+ }
+
+ /**
+ * Optional: Sets logo description text that will be shown on the prompt.
+ *
+ * <p> Note that using this method is not recommended in most scenarios because the
+ * calling application's name will be used by default. Setting the logo description
+ * is intended for large bundled applications that perform a wide range of functions
+ * and need to show distinct description for each function.
+ *
+ * @param logoDescription The logo description text that will be shown on the prompt.
+ * @return This builder.
+ * @throws IllegalArgumentException If logo description is null or exceeds certain
+ * character limit.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @NonNull
+ public Builder setLogoDescription(@NonNull String logoDescription) {
+ mLogoDescription = logoDescription;
+ return this;
+ }
+
+ /**
* Required: Sets the title for the prompt.
*
* @param title The title to be displayed on the prompt.
@@ -471,9 +593,14 @@
}
/**
- * Optional: Sets the description for the prompt.
+ * Optional: Sets a description that will be shown on the prompt.
*
- * @param description The description to be displayed on the prompt.
+ * <p> Note that the description set by {@link Builder#setDescription(CharSequence)}
+ * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view
+ * provided to {@link Builder#setContentView(PromptContentView)} will be used if both
+ * methods are called.
+ *
+ * @param description The description to display.
* @return This builder.
*/
@NonNull
@@ -483,6 +610,23 @@
}
/**
+ * Optional: Sets application customized content view that will be shown on the prompt.
+ *
+ * <p> Note that the description set by {@link Builder#setDescription(CharSequence)}
+ * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view
+ * provided to {@link Builder#setContentView(PromptContentView)} will be used if both
+ * methods are called.
+ *
+ * @param view The customized view information.
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setContentView(@NonNull PromptContentView view) {
+ mPromptContentView = view;
+ return this;
+ }
+
+ /**
* Required: Sets the text for the negative button on the prompt.
*
* <p>Note that this option is incompatible with device credential authentication and
@@ -623,9 +767,13 @@
}
return new PromptInfo(
+ mLogoRes,
+ mLogoBitmap,
+ mLogoDescription,
mTitle,
mSubtitle,
mDescription,
+ mPromptContentView,
mNegativeButtonText,
mIsConfirmationRequired,
mIsDeviceCredentialAllowed,
@@ -634,9 +782,13 @@
}
// Immutable fields for the prompt info object.
+ @DrawableRes private int mLogoRes;
+ @Nullable private Bitmap mLogoBitmap;
+ @Nullable private String mLogoDescription;
@NonNull private final CharSequence mTitle;
@Nullable private final CharSequence mSubtitle;
@Nullable private final CharSequence mDescription;
+ @Nullable private PromptContentView mPromptContentView;
@Nullable private final CharSequence mNegativeButtonText;
private final boolean mIsConfirmationRequired;
private final boolean mIsDeviceCredentialAllowed;
@@ -645,16 +797,24 @@
// Prevent direct instantiation.
@SuppressWarnings("WeakerAccess") /* synthetic access */
PromptInfo(
+ int logoRes,
+ @Nullable Bitmap logoBitmap,
+ @Nullable String logoDescription,
@NonNull CharSequence title,
@Nullable CharSequence subtitle,
@Nullable CharSequence description,
+ @Nullable PromptContentView promptContentView,
@Nullable CharSequence negativeButtonText,
boolean confirmationRequired,
boolean deviceCredentialAllowed,
@BiometricManager.AuthenticatorTypes int allowedAuthenticators) {
+ mLogoRes = logoRes;
+ mLogoBitmap = logoBitmap;
+ mLogoDescription = logoDescription;
mTitle = title;
mSubtitle = subtitle;
mDescription = description;
+ mPromptContentView = promptContentView;
mNegativeButtonText = negativeButtonText;
mIsConfirmationRequired = confirmationRequired;
mIsDeviceCredentialAllowed = deviceCredentialAllowed;
@@ -662,6 +822,44 @@
}
/**
+ * Gets the drawable resource of the logo for the prompt, as set by
+ * {@link Builder#setLogoRes(int)}. Currently for system applications use only.
+ *
+ * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @DrawableRes
+ public int getLogoRes() {
+ return mLogoRes;
+ }
+
+ /**
+ * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}.
+ * Currently for system applications use only.
+ *
+ * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @Nullable
+ public Bitmap getLogoBitmap() {
+ return mLogoBitmap;
+ }
+
+ /**
+ * Gets the logo description for the prompt, as set by
+ * {@link Builder#setLogoDescription(String)}.
+ * Currently for system applications use only.
+ *
+ * @return The logo description of the prompt, or null if the prompt has no logo description
+ * set.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @Nullable
+ public String getLogoDescription() {
+ return mLogoDescription;
+ }
+
+ /**
* Gets the title for the prompt.
*
* @return The title to be displayed on the prompt.
@@ -698,6 +896,17 @@
}
/**
+ * Gets the content view for the prompt, as set by
+ * {@link Builder#setContentView(PromptContentView)}.
+ *
+ * @return The content view for the prompt, or null if the prompt has no content view.
+ */
+ @Nullable
+ public PromptContentView getContentView() {
+ return mPromptContentView;
+ }
+
+ /**
* Gets the text for the negative button on the prompt.
*
* @return The label to be used for the negative button on the prompt, or an empty string if
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
index 56f8782..eb6970f 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
@@ -16,7 +16,9 @@
package androidx.biometric;
+import android.annotation.SuppressLint;
import android.content.DialogInterface;
+import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
@@ -139,6 +141,29 @@
}
/**
+ * The dialog listener that is returned by {@link #getMoreOptionsButtonListener()} ()}.
+ */
+ private static class MoreOptionsButtonListener implements DialogInterface.OnClickListener {
+ @NonNull private final WeakReference<BiometricViewModel> mViewModelRef;
+
+ /**
+ * Creates a more options button listener with a weak reference to the given view model.
+ *
+ * @param viewModel The view model instance to hold a weak reference to.
+ */
+ MoreOptionsButtonListener(@Nullable BiometricViewModel viewModel) {
+ mViewModelRef = new WeakReference<>(viewModel);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int which) {
+ if (mViewModelRef.get() != null) {
+ mViewModelRef.get().setMoreOptionsButtonPressPending(true);
+ }
+ }
+ }
+
+ /**
* The executor that will run authentication callback methods.
*
* <p>If unset, callbacks are invoked on the main thread with {@link Looper#getMainLooper()}.
@@ -181,6 +206,11 @@
@Nullable private DialogInterface.OnClickListener mNegativeButtonListener;
/**
+ * A dialog listener for the more options button shown on the prompt content.
+ */
+ @Nullable private DialogInterface.OnClickListener mMoreOptionsButtonListener;
+
+ /**
* A label for the negative button shown on the prompt.
*
* <p>If set, this value overrides the one returned by
@@ -251,6 +281,11 @@
@Nullable private MutableLiveData<Boolean> mIsNegativeButtonPressPending;
/**
+ * Whether the user has pressed the more options button on the prompt content.
+ */
+ @Nullable private MutableLiveData<Boolean> mIsMoreOptionsButtonPressPending;
+
+ /**
* Whether the fingerprint dialog should always be dismissed instantly.
*/
private boolean mIsFingerprintDialogDismissedInstantly = true;
@@ -327,6 +362,47 @@
}
/**
+ * Gets the logo res to be shown on the biometric prompt.
+ *
+ * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+ *
+ * @return The logo res for the prompt, or -1 if not set.
+ */
+ @SuppressLint("MissingPermission")
+ int getLogoRes() {
+ return mPromptInfo != null ? mPromptInfo.getLogoRes() : -1;
+ }
+
+ /**
+ * Gets the logo bitmap to be shown on the biometric prompt.
+ *
+ * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+ *
+ * @return The logo bitmap for the prompt, or null if not set.
+ */
+ @SuppressLint("MissingPermission")
+ @Nullable
+ Bitmap getLogoBitmap() {
+ return mPromptInfo != null ? mPromptInfo.getLogoBitmap() : null;
+ }
+
+ /**
+ * Gets the logo description to be shown on the biometric prompt.
+ *
+ * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+ *
+ * @return The logo description for the prompt, or null if not set.
+ */
+ @SuppressLint("MissingPermission")
+ @Nullable
+ String getLogoDescription() {
+ return mPromptInfo != null ? mPromptInfo.getLogoDescription() : null;
+ }
+
+ /**
* Gets the title to be shown on the biometric prompt.
*
* <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
@@ -366,6 +442,19 @@
}
/**
+ * Gets the prompt content view to be shown on the biometric prompt.
+ *
+ * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+ *
+ * @return The prompt content view for the prompt, or {@code null} if not set.
+ */
+ @Nullable
+ PromptContentView getContentView() {
+ return mPromptInfo != null ? mPromptInfo.getContentView() : null;
+ }
+
+ /**
* Gets the text that should be shown for the negative button on the biometric prompt.
*
* <p>If non-null, the value set by {@link #setNegativeButtonTextOverride(CharSequence)} is
@@ -454,6 +543,14 @@
return mNegativeButtonListener;
}
+ @NonNull
+ DialogInterface.OnClickListener getMoreOptionsButtonListener() {
+ if (mMoreOptionsButtonListener == null) {
+ mMoreOptionsButtonListener = new MoreOptionsButtonListener(this);
+ }
+ return mMoreOptionsButtonListener;
+ }
+
void setNegativeButtonTextOverride(@Nullable CharSequence negativeButtonTextOverride) {
mNegativeButtonTextOverride = negativeButtonTextOverride;
}
@@ -593,6 +690,22 @@
updateValue(mIsNegativeButtonPressPending, negativeButtonPressPending);
}
+ @NonNull
+ LiveData<Boolean> isMoreOptionsButtonPressPending() {
+ if (mIsMoreOptionsButtonPressPending == null) {
+ mIsMoreOptionsButtonPressPending = new MutableLiveData<>();
+ }
+ return mIsMoreOptionsButtonPressPending;
+ }
+
+ void setMoreOptionsButtonPressPending(boolean moreOptionsButtonPressPending) {
+ if (mIsMoreOptionsButtonPressPending == null) {
+ mIsMoreOptionsButtonPressPending = new MutableLiveData<>();
+ }
+ updateValue(mIsMoreOptionsButtonPressPending, moreOptionsButtonPressPending);
+ }
+
+
boolean isFingerprintDialogDismissedInstantly() {
return mIsFingerprintDialogDismissedInstantly;
}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
index 4cc0c30..871135d 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
@@ -112,6 +112,17 @@
}
}
+ // Operation handle is only supported on API 35 and above.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // This should be the bottom one and only be reachable when cryptoObject was
+ // constructed with operation handle. cryptoObject from other constructors should
+ // already be unwrapped and returned above.
+ final long operationHandle = Api35Impl.getOperationHandle(cryptoObject);
+ if (operationHandle != 0) {
+ return new BiometricPrompt.CryptoObject(operationHandle);
+ }
+ }
+
return null;
}
@@ -164,10 +175,39 @@
}
}
+ // Operation handle is only supported on API 35 and above.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ final long operationHandle = cryptoObject.getOperationHandleCryptoObject();
+ if (operationHandle != 0) {
+ return Api35Impl.create(operationHandle);
+ }
+ }
+
return null;
}
/**
+ * Get the {@code operationHandle} associated with this object or 0 if none. This needs to be
+ * achieved by getting the corresponding
+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} and then get its
+ * operation handle.
+ *
+ * @param cryptoObject An instance of {@link androidx.biometric.BiometricPrompt.CryptoObject}.
+ * @return The {@code operationHandle} associated with this object or 0 if none.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ static long getOperationHandle(@Nullable BiometricPrompt.CryptoObject cryptoObject) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCryptoObject =
+ CryptoObjectUtils.wrapForBiometricPrompt(cryptoObject);
+ if (wrappedCryptoObject != null) {
+ return Api35Impl.getOperationHandle(wrappedCryptoObject);
+ }
+ }
+ return 0;
+ }
+
+ /**
* Unwraps a crypto object returned by
* {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}.
*
@@ -250,6 +290,11 @@
return null;
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ Log.e(TAG, "Operation handle is not supported by FingerprintManager.");
+ return null;
+ }
+
return null;
}
@@ -298,6 +343,41 @@
}
/**
+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private static class Api35Impl {
+ // Prevent instantiation.
+ private Api35Impl() {}
+
+ /**
+ * Creates an instance of the framework class
+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} from the given
+ * operation handle.
+ *
+ * @param operationHandle The operation handle to be wrapped.
+ * @return An instance of {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
+ */
+ @NonNull
+ static android.hardware.biometrics.BiometricPrompt.CryptoObject create(
+ long operationHandle) {
+ return new android.hardware.biometrics.BiometricPrompt.CryptoObject(operationHandle);
+ }
+
+ /**
+ * Gets the operation handle associated with the given crypto object, if any.
+ *
+ * @param crypto An instance of
+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
+ * @return The wrapped operation handle object, or {@code null}.
+ */
+ static long getOperationHandle(
+ @NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject crypto) {
+ return crypto.getOperationHandle();
+ }
+ }
+
+ /**
* Nested class to avoid verification errors for methods introduced in Android 13.0 (API 33).
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
new file mode 100644
index 0000000..099094a
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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.biometric;
+
+/**
+ * An item shown on {@link PromptContentView}.
+ */
+public interface PromptContentItem {
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java
new file mode 100644
index 0000000..6ec55e2
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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.biometric;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ */
+public final class PromptContentItemBulletedText implements PromptContentItem {
+ private final String mText;
+
+ /**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ *
+ * @param text The text of this list item.
+ */
+ public PromptContentItemBulletedText(@NonNull String text) {
+ mText = text;
+ }
+
+ @NonNull
+ String getText() {
+ return mText;
+ }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java
new file mode 100644
index 0000000..c7f2b6f
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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.biometric;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ */
+public final class PromptContentItemPlainText implements PromptContentItem {
+ private final String mText;
+
+ /**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ *
+ * @param text The text of this list item.
+ */
+ public PromptContentItemPlainText(@NonNull String text) {
+ mText = text;
+ }
+
+ @NonNull
+ String getText() {
+ return mText;
+ }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
new file mode 100644
index 0000000..090960b
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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.biometric;
+
+/**
+ * Contains the information of the template of content view for Biometric Prompt.
+ */
+public interface PromptContentView {
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
new file mode 100644
index 0000000..cb6679a8
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 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.biometric;
+
+import android.annotation.SuppressLint;
+import android.content.DialogInterface;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Utility class for creating and converting between different types of prompt content view that may
+ * be used internally by {@link BiometricPrompt}
+ */
+class PromptContentViewUtils {
+ // Prevent instantiation.
+ private PromptContentViewUtils() {
+ }
+
+ /**
+ * Wraps a prompt content view to be passed to {@link BiometricPrompt}.
+ *
+ * @param contentView An instance of {@link PromptContentView}.
+ * @param executor An executor for the more options button callback.
+ * @param moreOptionsButtonListener A listener for the more options button press event.
+ * @return An equivalent prompt content view that is compatible with
+ * {@link android.hardware.biometrics.PromptContentView}.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Nullable
+ static android.hardware.biometrics.PromptContentView wrapForBiometricPrompt(
+ @Nullable PromptContentView contentView, @NonNull Executor executor,
+ @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
+
+ if (contentView == null) {
+ return null;
+ }
+
+ // Prompt content view is only supported on API 35 and above.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (contentView instanceof PromptVerticalListContentView) {
+ return Api35Impl.createPromptVerticalListContentView(
+ (PromptVerticalListContentView) contentView);
+ } else if (contentView instanceof PromptContentViewWithMoreOptionsButton) {
+ return Api35Impl.createPromptContentViewWithMoreOptionsButton(
+ (PromptContentViewWithMoreOptionsButton) contentView, executor,
+ moreOptionsButtonListener);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @SuppressLint("MissingPermission")
+ private static class Api35Impl {
+ // Prevent instantiation.
+ private Api35Impl() {
+ }
+
+ /**
+ * Creates an instance of the framework class
+ * {@link android.hardware.biometrics.PromptVerticalListContentView} from the given
+ * content view.
+ *
+ * @param contentView The prompt content view to be wrapped.
+ * @return An instance of {@link android.hardware.biometrics.PromptVerticalListContentView}.
+ */
+ @NonNull
+ static android.hardware.biometrics.PromptContentView createPromptVerticalListContentView(
+ @NonNull PromptVerticalListContentView contentView) {
+ android.hardware.biometrics.PromptVerticalListContentView.Builder
+ contentViewBuilder =
+ new android.hardware.biometrics.PromptVerticalListContentView.Builder();
+ if (contentView.getDescription() != null) {
+ contentViewBuilder.setDescription(contentView.getDescription());
+ }
+ contentView.getListItems().forEach(
+ it -> {
+ if (it instanceof PromptContentItemPlainText) {
+ contentViewBuilder.addListItem(
+ new android.hardware.biometrics.PromptContentItemPlainText(
+ ((PromptContentItemPlainText) it).getText()));
+ } else if (it instanceof PromptContentItemBulletedText) {
+ contentViewBuilder.addListItem(
+ new android.hardware.biometrics.PromptContentItemBulletedText(
+ ((PromptContentItemBulletedText) it).getText()));
+ }
+ });
+ return contentViewBuilder.build();
+ }
+
+ /**
+ * Creates an instance of the framework class
+ * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton} from the
+ * given content view.
+ *
+ * @param contentView The prompt content view to be wrapped.
+ * @param executor An executor for the more options button callback.
+ * @param moreOptionsButtonListener A listener for the more options button press event.
+ * @return An instance of
+ * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton}.
+ */
+ @NonNull
+ static android.hardware.biometrics.PromptContentView
+ createPromptContentViewWithMoreOptionsButton(
+ @NonNull PromptContentViewWithMoreOptionsButton contentView,
+ @NonNull Executor executor,
+ @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
+ android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder
+ contentViewBuilder =
+ new android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
+ .Builder();
+ if (contentView.getDescription() != null) {
+ contentViewBuilder.setDescription(contentView.getDescription());
+ }
+ contentViewBuilder.setMoreOptionsButtonListener(executor, moreOptionsButtonListener);
+ return contentViewBuilder.build();
+ }
+ }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
new file mode 100644
index 0000000..eb4f9aa
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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.biometric;
+
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+
+/**
+ * Contains the information of the template of content view with a more options button for
+ * Biometric Prompt.
+ * <p>
+ * This button should be used to provide more options for sign in or other purposes, such as when a
+ * user needs to select between multiple app-specific accounts or profiles that are available for
+ * sign in.
+ * <p>
+ * Apps should avoid using this when possible because it will create additional steps that the user
+ * must navigate through - clicking the more options button will dismiss the prompt, provide the app
+ * an opportunity to ask the user for the correct option, and finally allow the app to decide how to
+ * proceed once selected.
+ *
+ * <p>
+ * Here's how you'd set a <code>PromptContentViewWithMoreOptionsButton</code> on a Biometric
+ * Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
+ * .setTitle(...)
+ * .setSubTitle(...)
+ * .setContentView(
+ * new PromptContentViewWithMoreOptionsButton.Builder()
+ * .setDescription("test description")
+ * .setMoreOptionsButtonListener(executor, listener)
+ * .build()
+ * )
+ * .build();
+ * </pre>
+ */
+public final class PromptContentViewWithMoreOptionsButton implements PromptContentView {
+ static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+
+ private final String mDescription;
+
+ private PromptContentViewWithMoreOptionsButton(@NonNull String description) {
+ mDescription = description;
+ }
+
+ /**
+ * Gets the description for the content view, as set by
+ * {@link PromptContentViewWithMoreOptionsButton.Builder#setDescription(String)}.
+ *
+ * @return The description for the content view, or null if the content view has no description.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * A builder used to set individual options for the
+ * {@link PromptContentViewWithMoreOptionsButton} class.
+ */
+ public static final class Builder {
+ private String mDescription;
+
+ /**
+ * Optional: Sets a description that will be shown on the content view.
+ *
+ * @param description The description to display.
+ * @return This builder.
+ * @throws IllegalArgumentException If description exceeds certain character limit.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @NonNull
+ public Builder setDescription(@NonNull String description) {
+ if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
+ throw new IllegalArgumentException("The character number of description exceeds "
+ + MAX_DESCRIPTION_CHARACTER_NUMBER);
+ }
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Creates a {@link PromptContentViewWithMoreOptionsButton}.
+ *
+ * @return An instance of {@link PromptContentViewWithMoreOptionsButton}.
+ */
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+ @NonNull
+ public PromptContentViewWithMoreOptionsButton build() {
+ return new PromptContentViewWithMoreOptionsButton(mDescription);
+ }
+ }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
new file mode 100644
index 0000000..58d7be2
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 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.biometric;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains the information of the template of vertical list content view for Biometric Prompt.
+ * <p>
+ * Here's how you'd set a <code>PromptVerticalListContentView</code> on a Biometric Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
+ * .setTitle(...)
+ * .setSubTitle(...)
+ * .setContentView(
+ * new PromptVerticalListContentView.Builder()
+ * .setDescription("test description")
+ * .addListItem(new PromptContentItemPlainText("test item 1"))
+ * .addListItem(new PromptContentItemPlainText("test item 2"))
+ * .addListItem(new PromptContentItemBulletedText("test item 3"))
+ * .build()
+ * )
+ * .build();
+ * </pre>
+ */
+public final class PromptVerticalListContentView implements PromptContentView {
+ static final int MAX_ITEM_NUMBER = 20;
+ static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+ static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+
+ private final List<PromptContentItem> mContentList;
+ private final String mDescription;
+
+ private PromptVerticalListContentView(@NonNull List<PromptContentItem> contentList,
+ @NonNull String description) {
+ mContentList = contentList;
+ mDescription = description;
+ }
+
+ /**
+ * Gets the description for the content view, as set by
+ * {@link PromptVerticalListContentView.Builder#setDescription(String)}.
+ *
+ * @return The description for the content view, or null if the content view has no description.
+ */
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Gets the list of items on the content view, as set by
+ * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}.
+ *
+ * @return The item list on the content view.
+ */
+ @NonNull
+ public List<PromptContentItem> getListItems() {
+ return new ArrayList<>(mContentList);
+ }
+
+ /**
+ * A builder used to set individual options for the {@link PromptVerticalListContentView} class.
+ */
+ public static final class Builder {
+ private final List<PromptContentItem> mContentList = new ArrayList<>();
+ private String mDescription;
+
+ /**
+ * Optional: Sets a description that will be shown on the content view.
+ *
+ * @param description The description to display.
+ * @return This builder.
+ * @throws IllegalArgumentException If description exceeds certain character limit.
+ */
+ @NonNull
+ public Builder setDescription(@NonNull String description) {
+ if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
+ throw new IllegalArgumentException("The character number of description exceeds "
+ + MAX_DESCRIPTION_CHARACTER_NUMBER);
+ }
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Optional: Adds a list item in the current row.
+ *
+ * @param listItem The list item view to display
+ * @return This builder.
+ * @throws IllegalArgumentException If this list item exceeds certain character limits or
+ * the number of list items exceeds certain limit.
+ */
+ @NonNull
+ public Builder addListItem(@NonNull PromptContentItem listItem) {
+ mContentList.add(listItem);
+ checkItemLimits(listItem);
+ return this;
+ }
+
+ /**
+ * Optional: Adds a list item in the current row.
+ *
+ * @param listItem The list item view to display
+ * @param index The position at which to add the item
+ * @return This builder.
+ * @throws IllegalArgumentException If this list item exceeds certain character limits or
+ * the number of list items exceeds certain limit.
+ */
+ @NonNull
+ public Builder addListItem(@NonNull PromptContentItem listItem, int index) {
+ mContentList.add(index, listItem);
+ checkItemLimits(listItem);
+ return this;
+ }
+
+ private void checkItemLimits(@NonNull PromptContentItem listItem) {
+ if (doesListItemExceedsCharLimit(listItem)) {
+ throw new IllegalArgumentException(
+ "The character number of list item exceeds "
+ + MAX_EACH_ITEM_CHARACTER_NUMBER);
+ }
+ if (mContentList.size() > MAX_ITEM_NUMBER) {
+ throw new IllegalArgumentException(
+ "The number of list items exceeds " + MAX_ITEM_NUMBER);
+ }
+ }
+
+ private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) {
+ if (listItem instanceof PromptContentItemPlainText) {
+ return ((PromptContentItemPlainText) listItem).getText().length()
+ > MAX_EACH_ITEM_CHARACTER_NUMBER;
+ } else if (listItem instanceof PromptContentItemBulletedText) {
+ return ((PromptContentItemBulletedText) listItem).getText().length()
+ > MAX_EACH_ITEM_CHARACTER_NUMBER;
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Creates a {@link PromptVerticalListContentView}.
+ *
+ * @return An instance of {@link PromptVerticalListContentView}.
+ */
+ @NonNull
+ public PromptVerticalListContentView build() {
+ return new PromptVerticalListContentView(mContentList, mDescription);
+ }
+ }
+}
+
diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
index 56a5342..a43ef4c 100644
--- a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
+++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.graphics.Bitmap;
import android.os.Build;
import androidx.biometric.BiometricManager.Authenticators;
@@ -58,6 +59,95 @@
assertThat(allowedAuthenticators).isEqualTo(allowedAuthenticators);
}
+ @Test
+ public void testPromptInfo_CanSetAndGetOptions_logoResAndDescription() {
+ final int logoRes = R.drawable.fingerprint_dialog_fp_icon;
+ final String logoDescription = "logo description";
+ final String title = "Title";
+ final String negativeButtonText = "Negative";
+
+ final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+ .setLogoRes(logoRes)
+ .setLogoDescription(logoDescription)
+ .setLogoDescription(logoDescription)
+ .setTitle(title)
+ .setNegativeButtonText(negativeButtonText)
+ .build();
+
+ assertThat(info.getLogoRes()).isEqualTo(logoRes);
+ assertThat(info.getLogoDescription()).isEqualTo(logoDescription);
+ }
+
+ @Test
+ public void testPromptInfo_CanSetAndGetOptions_logoBitmap() {
+ final Bitmap logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565);
+ final String logoDescription = "logo description";
+ final String title = "Title";
+ final String negativeButtonText = "Negative";
+
+ final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+ .setLogoBitmap(logoBitmap)
+ .setLogoDescription(logoDescription)
+ .setTitle(title)
+ .setNegativeButtonText(negativeButtonText)
+ .build();
+
+ assertThat(info.getLogoBitmap()).isEqualTo(logoBitmap);
+ }
+
+ @Test
+ public void testPromptInfo_CanSetAndGetOptions_verticalListContent() {
+ final String contentDescription = "test description";
+ final String itemOne = "content item 1";
+ final String itemTwo = "content item 2";
+ final PromptVerticalListContentView contentView =
+ new PromptVerticalListContentView.Builder()
+ .setDescription(contentDescription)
+ .addListItem(new PromptContentItemBulletedText(itemOne))
+ .addListItem(new PromptContentItemBulletedText(itemTwo), 1).build();
+ final String title = "Title";
+ final String negativeButtonText = "Negative";
+
+ final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setNegativeButtonText(negativeButtonText)
+ .setContentView(contentView)
+ .build();
+
+ assertThat(info.getContentView()).isEqualTo(contentView);
+ final PromptVerticalListContentView realContentView =
+ (PromptVerticalListContentView) info.getContentView();
+ assertThat(realContentView.getDescription()).isEqualTo(contentDescription);
+ final PromptContentItemBulletedText realItemOne =
+ (PromptContentItemBulletedText) realContentView.getListItems().get(0);
+ assertThat(realItemOne.getText()).isEqualTo(itemOne);
+ final PromptContentItemBulletedText realItemTwo =
+ (PromptContentItemBulletedText) realContentView.getListItems().get(1);
+ assertThat(realItemTwo.getText()).isEqualTo(itemTwo);
+
+ }
+
+ @Test
+ public void testPromptInfo_CanSetAndGetOptions_contentViewMoreOptionsButton() {
+ final String contentDescription = "test description";
+ final PromptContentViewWithMoreOptionsButton contentView =
+ new PromptContentViewWithMoreOptionsButton.Builder().setDescription(
+ contentDescription).build();
+ final String title = "Title";
+ final String negativeButtonText = "Negative";
+
+ final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setNegativeButtonText(negativeButtonText)
+ .setContentView(contentView)
+ .build();
+
+ assertThat(info.getContentView()).isEqualTo(contentView);
+ assertThat(
+ ((PromptContentViewWithMoreOptionsButton) info.getContentView())
+ .getDescription()).isEqualTo(contentDescription);
+ }
+
@Test(expected = IllegalArgumentException.class)
public void testPromptInfo_FailsToBuild_WithNoTitle() {
new BiometricPrompt.PromptInfo.Builder().setNegativeButtonText("Cancel").build();
diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
index 51e0889..f935035 100644
--- a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
+++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
@@ -36,13 +36,13 @@
}
@Test
- public void testCanUpdateLiveDataValue_OnMainThread() {
+ public void testCanUpdateNegativeButtonLiveDataValue_OnMainThread() {
mViewModel.setNegativeButtonPressPending(true);
assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
}
@Test
- public void testCanUpdateLiveDataValue_OnBackgroundThread() throws Exception {
+ public void testCanUpdateNegativeButtonLiveDataValue_OnBackgroundThread() throws Exception {
final Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
@@ -54,4 +54,24 @@
ShadowLooper.runUiThreadTasks();
assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
}
+
+ @Test
+ public void testCanUpdateMoreOptionsButtonLiveDataValue_OnMainThread() {
+ mViewModel.setMoreOptionsButtonPressPending(true);
+ assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
+ }
+
+ @Test
+ public void testCanUpdateMoreOptionsButtonLiveDataValue_OnBackgroundThread() throws Exception {
+ final Thread backgroundThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ mViewModel.setMoreOptionsButtonPressPending(true);
+ }
+ });
+ backgroundThread.start();
+ backgroundThread.join();
+ ShadowLooper.runUiThreadTasks();
+ assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
+ }
}
diff --git a/biometric/integration-tests/testapp/build.gradle b/biometric/integration-tests/testapp/build.gradle
index e36cafe..0b42c40 100644
--- a/biometric/integration-tests/testapp/build.gradle
+++ b/biometric/integration-tests/testapp/build.gradle
@@ -21,6 +21,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.biometric.integration.testapp"
}
diff --git a/camera/camera-feature-combination-query-play-services/build.gradle b/camera/camera-feature-combination-query-play-services/build.gradle
index 721b314..de58db5 100644
--- a/camera/camera-feature-combination-query-play-services/build.gradle
+++ b/camera/camera-feature-combination-query-play-services/build.gradle
@@ -24,6 +24,7 @@
dependencies {
api(libs.androidx.annotation)
+ project(":camera:camera-feature-combination-query")
implementation(project(":camera:camera-feature-combination-query"))
testImplementation(libs.testRunner)
@@ -32,9 +33,16 @@
testImplementation(libs.truth)
testImplementation(libs.testRules)
testImplementation(libs.testCore)
+
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
}
android {
+ compileSdk 35
lintOptions {
enable 'CameraXQuirksClassDetector'
}
@@ -53,4 +61,4 @@
"library providing camera feature combination with Google Play Services dependencies."
metalavaK2UastEnabled = true
legacyDisableKotlinStrictApiMode = true
-}
\ No newline at end of file
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt b/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt
new file mode 100644
index 0000000..e2c097b
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery.playservices
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.params.SessionConfiguration
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_PLAY_SERVICES
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 28)
+class PlayServicesCameraDeviceSetupCompatTest {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ @Test
+ fun queryResult_resultSourceIsPlayServices() {
+ // Arrange.
+ val factory = CameraDeviceSetupCompatFactory(instrumentation.context)
+ val impl = factory.getCameraDeviceSetupCompat("1")
+ val sessionConfiguration =
+ SessionConfiguration(
+ SessionConfiguration.SESSION_REGULAR,
+ listOf(),
+ directExecutor(),
+ object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(p0: CameraCaptureSession) {
+ // no-op
+ }
+
+ override fun onConfigureFailed(p0: CameraCaptureSession) {
+ // no-op
+ }
+ }
+ )
+ // Act.
+ val result = impl.isSessionConfigurationSupported(sessionConfiguration)
+ // Assert.
+ assertThat(result.source).isEqualTo(SOURCE_PLAY_SERVICES)
+ assertThat(result.supported).isEqualTo(SupportQueryResult.RESULT_UNSUPPORTED)
+ assertThat(result.timestampMillis).isEqualTo(0)
+ }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml b/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
index a5918e6..9a67ab2 100644
--- a/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
+++ b/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
@@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <application>
+ <service
+ android:name="androidx.camera.featurecombinationquery.playservices.MetadataHolderService"
+ android:enabled="true"
+ android:exported="false"
+ tools:ignore="MissingServiceExportedEqualsTrue">
+ <meta-data
+ android:name="androidx.camera.featurecombinationquery.PLAY_SERVICES_IMPL_PROVIDER_KEY"
+ android:value="androidx.camera.featurecombinationquery.playservices.PlayServicesCameraDeviceSetupCompatProvider" />
+ </service>
+ </application>
</manifest>
\ No newline at end of file
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java
new file mode 100644
index 0000000..48e84db
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery.playservices;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Service that holds metadata for the Play Services CameraDeviceSetup implementation.
+ */
+public class MetadataHolderService extends Service {
+
+ @Nullable
+ @Override
+ public IBinder onBind(@Nullable Intent intent) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java
new file mode 100644
index 0000000..9e53d6a
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery.playservices;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNSUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_PLAY_SERVICES;
+
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory;
+
+/**
+ * A Google Play Services based {@link CameraDeviceSetupCompat} implementation.
+ *
+ * <p>This class is internal only and app cannot instantiate it directly. Instead app will
+ * depend on the camera-feature-combination-query-play-services artifact to get an instance of
+ * this class via the {@link CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat} API.
+ */
+public class PlayServicesCameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+ public PlayServicesCameraDeviceSetupCompat(@NonNull String cameraId) {
+ // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+ }
+
+ @NonNull
+ @Override
+ public SupportQueryResult isSessionConfigurationSupported(
+ @NonNull SessionConfiguration sessionConfig) {
+ // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+ return new SupportQueryResult(RESULT_UNSUPPORTED, SOURCE_PLAY_SERVICES, 0);
+ }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..367ca54
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery.playservices;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatProvider;
+
+/**
+ * A Google Play Services based {@link CameraDeviceSetupCompat} implementation.
+ *
+ * <p>This class is internal only and app cannot instantiate it directly. Instead app will
+ * depend on the camera-feature-combination-query-play-services artifact to get an instance of
+ * this class via the {@link CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat} API.
+ */
+public class PlayServicesCameraDeviceSetupCompatProvider implements
+ CameraDeviceSetupCompatProvider {
+
+ public PlayServicesCameraDeviceSetupCompatProvider(@NonNull Context context) {
+ // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+ }
+
+ @NonNull
+ @Override
+ public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId) {
+ return new PlayServicesCameraDeviceSetupCompat(cameraId);
+ }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java
new file mode 100644
index 0000000..4d36705
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.featurecombinationquery.playservices;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-feature-combination-query/api/1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
+++ b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+ public interface CameraDeviceSetupCompat {
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ public static final class CameraDeviceSetupCompat.SupportQueryResult {
+ ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+ method public int getSource();
+ method public int getSupported();
+ method public long getTimestampMillis();
+ field public static final int RESULT_SUPPORTED = 1; // 0x1
+ field public static final int RESULT_UNDEFINED = 0; // 0x0
+ field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+ field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+ field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+ field public static final int SOURCE_UNDEFINED = 0; // 0x0
+ }
+
+ public class CameraDeviceSetupCompatFactory {
+ ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+ }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/current.txt b/camera/camera-feature-combination-query/api/current.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/current.txt
+++ b/camera/camera-feature-combination-query/api/current.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+ public interface CameraDeviceSetupCompat {
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ public static final class CameraDeviceSetupCompat.SupportQueryResult {
+ ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+ method public int getSource();
+ method public int getSupported();
+ method public long getTimestampMillis();
+ field public static final int RESULT_SUPPORTED = 1; // 0x1
+ field public static final int RESULT_UNDEFINED = 0; // 0x0
+ field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+ field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+ field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+ field public static final int SOURCE_UNDEFINED = 0; // 0x0
+ }
+
+ public class CameraDeviceSetupCompatFactory {
+ ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+ }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
+++ b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+ public interface CameraDeviceSetupCompat {
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ public static final class CameraDeviceSetupCompat.SupportQueryResult {
+ ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+ method public int getSource();
+ method public int getSupported();
+ method public long getTimestampMillis();
+ field public static final int RESULT_SUPPORTED = 1; // 0x1
+ field public static final int RESULT_UNDEFINED = 0; // 0x0
+ field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+ field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+ field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+ field public static final int SOURCE_UNDEFINED = 0; // 0x0
+ }
+
+ public class CameraDeviceSetupCompatFactory {
+ ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+ }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/restricted_current.txt b/camera/camera-feature-combination-query/api/restricted_current.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/restricted_current.txt
+++ b/camera/camera-feature-combination-query/api/restricted_current.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+ public interface CameraDeviceSetupCompat {
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ public static final class CameraDeviceSetupCompat.SupportQueryResult {
+ ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+ method public int getSource();
+ method public int getSupported();
+ method public long getTimestampMillis();
+ field public static final int RESULT_SUPPORTED = 1; // 0x1
+ field public static final int RESULT_UNDEFINED = 0; // 0x0
+ field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+ field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+ field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+ field public static final int SOURCE_UNDEFINED = 0; // 0x0
+ }
+
+ public class CameraDeviceSetupCompatFactory {
+ ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+ method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+ }
+
+ @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+ }
+
+}
+
diff --git a/camera/camera-feature-combination-query/build.gradle b/camera/camera-feature-combination-query/build.gradle
index 12d1d72..6d13031 100644
--- a/camera/camera-feature-combination-query/build.gradle
+++ b/camera/camera-feature-combination-query/build.gradle
@@ -24,6 +24,7 @@
dependencies {
api(libs.androidx.annotation)
+ implementation(libs.autoValueAnnotations)
testImplementation(libs.testRunner)
testImplementation(libs.robolectric)
@@ -31,6 +32,8 @@
testImplementation(libs.truth)
testImplementation(libs.testRules)
testImplementation(libs.testCore)
+
+ annotationProcessor(libs.autoValue)
}
android {
@@ -38,6 +41,7 @@
enable 'CameraXQuirksClassDetector'
}
+ compileSdk 35
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.camera.featurecombinationquery"
}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java
new file mode 100644
index 0000000..6fc6e84
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNDEFINED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_UNDEFINED;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * A {@link CameraDeviceSetupCompat} implementation that combines multiple
+ * {@link CameraDeviceSetupCompat}.
+ *
+ * <p>This class checks if a {@link SessionConfiguration} is supported in the order of the
+ * provided implementation list, and returns the first non-undefined result. If all results are
+ * undefined, it will return a undefined result.
+ */
+final class AggregatedCameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+ private final List<CameraDeviceSetupCompat> mCameraDeviceSetupImpls;
+
+ AggregatedCameraDeviceSetupCompat(List<CameraDeviceSetupCompat> cameraDeviceSetupImpls) {
+ mCameraDeviceSetupImpls = cameraDeviceSetupImpls;
+ }
+
+ @NonNull
+ @Override
+ public SupportQueryResult isSessionConfigurationSupported(
+ @NonNull SessionConfiguration sessionConfig)
+ throws CameraAccessException {
+ for (CameraDeviceSetupCompat impl : mCameraDeviceSetupImpls) {
+ SupportQueryResult result = impl.isSessionConfigurationSupported(sessionConfig);
+ if (result.getSupported() != RESULT_UNDEFINED) {
+ return result;
+ }
+ }
+ return new SupportQueryResult(RESULT_UNDEFINED, SOURCE_UNDEFINED, 0);
+ }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java
new file mode 100644
index 0000000..dc4fce8
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_SUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNSUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_ANDROID_FRAMEWORK;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A Android framework based {@link CameraDeviceSetupCompat} implementation.
+ */
+@RequiresApi(api = 35)
+class Camera2CameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+ private final CameraDevice.CameraDeviceSetup mCameraDeviceSetup;
+
+ Camera2CameraDeviceSetupCompat(@NonNull CameraManager cameraManager, @NonNull String cameraId)
+ throws CameraAccessException {
+ mCameraDeviceSetup = cameraManager.getCameraDeviceSetup(cameraId);
+ }
+
+ @NonNull
+ @Override
+ public SupportQueryResult isSessionConfigurationSupported(
+ @NonNull SessionConfiguration sessionConfig)
+ throws CameraAccessException {
+ return new SupportQueryResult(
+ mCameraDeviceSetup.isSessionConfigurationSupported(sessionConfig) ? RESULT_SUPPORTED
+ : RESULT_UNSUPPORTED,
+ SOURCE_ANDROID_FRAMEWORK,
+ getBuildTimeEpochMillis());
+ }
+
+ public static long getBuildTimeEpochMillis() {
+ String value = System.getProperty("ro.build.date.utc");
+ if (value != null) {
+ try {
+ return Long.parseLong(value) * 1000;
+ } catch (NumberFormatException e) {
+ // Fall through
+ }
+ }
+ return 0;
+ }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..aa8eceb
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A Android framework based {@link CameraDeviceSetupCompat} implementation.
+ */
+@RequiresApi(api = 35)
+class Camera2CameraDeviceSetupCompatProvider implements CameraDeviceSetupCompatProvider {
+
+ private final CameraManager mCameraManager;
+
+ Camera2CameraDeviceSetupCompatProvider(@NonNull Context context) {
+ mCameraManager = context.getSystemService(CameraManager.class);
+ }
+
+ @NonNull
+ @Override
+ public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+ throws CameraAccessException {
+ return new Camera2CameraDeviceSetupCompat(mCameraManager, cameraId);
+ }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java
new file mode 100644
index 0000000..e856e7e
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Interface for checking if a {@link SessionConfiguration} is supported by the device.
+ *
+ * <p>This interface is a compatible version of the {@link CameraDevice.CameraDeviceSetup}
+ * class.
+ *
+ * <p>Implementations of this interface must be able to check if a {@link SessionConfiguration}
+ * is supported. They will check both the output streams and the session parameters, then return
+ * whether the combination works for the given camera. For example, a camera device may support
+ * HDR and 60FPS frame rate, but not both at the same time.
+ *
+ * @see CameraDevice.CameraDeviceSetup
+ */
+public interface CameraDeviceSetupCompat {
+
+ /**
+ * Checks if the {@link SessionConfiguration} is supported.
+ *
+ * @param sessionConfig The {@link SessionConfiguration} to check.
+ * @return a {@link SupportQueryResult} indicating if the {@link SessionConfiguration} is
+ * supported.
+ * @throws CameraAccessException if the camera device is no longer connected or has
+ * encountered a fatal error.
+ * @see CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported
+ */
+ @NonNull
+ SupportQueryResult isSessionConfigurationSupported(@NonNull SessionConfiguration sessionConfig)
+ throws CameraAccessException;
+
+ /**
+ * Result of a {@link CameraDeviceSetupCompat#isSessionConfigurationSupported} query.
+ */
+ final class SupportQueryResult {
+
+ /**
+ * Source of the result is undefined. This is always accompanied by
+ * {@link #RESULT_UNDEFINED}.
+ */
+ public static final int SOURCE_UNDEFINED = 0;
+ /**
+ * Source of the result is Google Play Services.
+ */
+ public static final int SOURCE_PLAY_SERVICES = 1;
+ /**
+ * Source of the result is Android framework.
+ */
+ public static final int SOURCE_ANDROID_FRAMEWORK = 2;
+
+ /**
+ * The library cannot determine if the {@link SessionConfiguration} is supported.
+ *
+ * <p>For API levels 29 to 34 inclusive, the app may continue to call
+ * {@link CameraDevice#isSessionConfigurationSupported} to check if the session
+ * configuration is supported.
+ *
+ * @see CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat for sample code.
+ */
+ public static final int RESULT_UNDEFINED = 0;
+ /**
+ * The {@link SessionConfiguration} is supported by the camera.
+ */
+ public static final int RESULT_SUPPORTED = 1;
+ /**
+ * The {@link SessionConfiguration} is not supported by the camera.
+ */
+ public static final int RESULT_UNSUPPORTED = 2;
+
+ /**
+ * Options for where the result is coming from.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {RESULT_UNDEFINED, RESULT_SUPPORTED, RESULT_UNSUPPORTED})
+ public @interface Supported {
+ }
+
+ /**
+ * Options for where the result is coming from.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {SOURCE_UNDEFINED, SOURCE_PLAY_SERVICES, SOURCE_ANDROID_FRAMEWORK})
+ @interface Sources {
+ }
+
+ // Whether the configuration is supported.
+ @Supported
+ private final int mSupported;
+ // The source of the result.
+ @Sources
+ private final int mSource;
+ // The timestamp of when the result was updated.
+ private final long mTimestampMillis;
+
+ /**
+ * Creates a new instance of {@link SupportQueryResult}.
+ *
+ * @param supported Whether the {@link SessionConfiguration} is supported.
+ * @param source The source of the result.
+ * @param timestampMillis The epoch timestamp of when the result was updated.
+ */
+ public SupportQueryResult(int supported, int source, long timestampMillis) {
+ mSupported = supported;
+ mSource = source;
+ mTimestampMillis = timestampMillis;
+ }
+
+ /**
+ * Whether the {@link SessionConfiguration} is supported.
+ *
+ * <p> If the value is {@link #RESULT_SUPPORTED}, the configuration is
+ * supported by the camera; if the value is {@link #RESULT_UNSUPPORTED}, the
+ * configuration is not supported by the camera; if the value is
+ * {@link #RESULT_UNDEFINED}, then the library cannot determine if the configuration is
+ * supported or not.
+ */
+ @Supported
+ public int getSupported() {
+ return mSupported;
+ }
+
+ /**
+ * Returns the source of the result.
+ *
+ * <p> If the source of the information is Play Services, the value is
+ * {@link #SOURCE_PLAY_SERVICES}; if the source is Android framework, the value is
+ * {@link #SOURCE_ANDROID_FRAMEWORK}; otherwise, the value is {@link #SOURCE_UNDEFINED}.
+ */
+ @Sources
+ public int getSource() {
+ return mSource;
+ }
+
+ /**
+ * Returns the epoch timestamp of when the result was updated.
+ *
+ * <p> If the source is {@link #SOURCE_PLAY_SERVICES}, the value is the time when the
+ * data is updated on the Play Services server; if the source is
+ * {@link #SOURCE_ANDROID_FRAMEWORK}, the value is the build property "ro.build.date.utc"
+ * if available; otherwise, it will return 0.
+ */
+ public long getTimestampMillis() {
+ return mTimestampMillis;
+ }
+ }
+
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java
new file mode 100644
index 0000000..34fb0d0
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory for creating {@link CameraDeviceSetupCompat} instances.
+ */
+public class CameraDeviceSetupCompatFactory {
+
+ private static final String PLAY_SERVICES_IMPL_KEY =
+ "androidx.camera.featurecombinationquery.PLAY_SERVICES_IMPL_PROVIDER_KEY";
+
+ private final Context mContext;
+
+ // Cached provider for Play Services implementation.
+ @Nullable
+ private CameraDeviceSetupCompatProvider mPlayServicesProvider;
+ // Cached provider for Camera2 implementation.
+ @Nullable
+ private CameraDeviceSetupCompatProvider mCamera2Provider;
+
+ /**
+ * Creates a new instance of {@link CameraDeviceSetupCompatFactory}.
+ *
+ * @param context The context to use for creating {@link CameraDeviceSetupCompat} instances.
+ */
+ public CameraDeviceSetupCompatFactory(@NonNull Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Gets a new instance of {@link CameraDeviceSetupCompat} for the given camera ID.
+ *
+ * <p> The returned instance aggregates the results from both the Play Services and the
+ * Android framework. It first checks if a Play Services implementation exists, and if so,
+ * return the query result from the Play Services implementation. If no Play Services
+ * implementation exists or the result from Play Services is undefined, the returned instance
+ * will then query Android framework for the result, if running on a new enough version.
+ * Sample code:
+ *
+ * <pre><code>
+ * // Query the compatibility before opening the camera.
+ * CameraDeviceSetupCompatFactory factory = new CameraDeviceSetupCompatFactory(context);
+ * CameraDeviceSetupCompat cameraDeviceSetupCompat
+ * = factory.getCameraDeviceSetupCompat(cameraId);
+ * Result result
+ * = cameraDeviceSetupCompat.isSessionConfigurationSupported(sessionConfiguration);
+ * boolean supported = result.getValue() == CameraDeviceSetupCompat.RESULT_SUPPORTED;
+ * </code></pre>
+ *
+ * <p> To include the Play Services as a source, the app must depend on the
+ * camera-feature-combination-query-play-services artifact.
+ *
+ * <p> If the return value is
+ * {@link CameraDeviceSetupCompat.SupportQueryResult#RESULT_UNDEFINED}, on API level between
+ * 29 and 34 inclusive, it's also possible to further check if the session
+ * configuration is supported by querying the
+ * {@link CameraDevice#isSessionConfigurationSupported}. This approach requires
+ * opening the camera first which may introduce latency, so the AndroidX implementation does not
+ * include this code path by default. Additionally, this approach does not check the values
+ * set in {@link SessionConfiguration#setSessionParameters}. For example, the FPS range is
+ * ignored. Sample code:
+ *
+ * <pre><code>
+ * if (SDK_INT <= 34 && SDK_INT >= 29) {
+ * // Check if the session configuration is supported with an opened CameraDevice.
+ * try {
+ * supported = supported || (result.getValue() == RESULT_UNDEFINED &&
+ * cameraDevice.isSessionConfigurationSupported(sessionConfiguration));
+ * } catch (UnsupportedOperationException unsupportedException) {
+ * // CameraDevice may throw UnsupportedOperationException when the config is not supported.
+ * }
+ * }
+ * </code></pre>
+ *
+ * @throws IllegalStateException If the Play Services implementation exists but the library
+ * fails to instantiate it. For example, if there are multiple
+ * Play Services implementations in the manifest.
+ * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not
+ * match any device in {@link CameraManager#getCameraIdList()}.
+ * @throws CameraAccessException If the camera has encountered a fatal error.
+ * @see CameraDevice#isSessionConfigurationSupported
+ * @see CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported
+ */
+ @NonNull
+ public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+ throws CameraAccessException {
+ List<CameraDeviceSetupCompat> impls = new ArrayList<>();
+ if (mPlayServicesProvider == null) {
+ // Create Play Services implementation if there isn't a cached one.
+ mPlayServicesProvider = getPlayServicesCameraDeviceSetupCompatProvider();
+ }
+ if (mPlayServicesProvider != null) {
+ // Add the Play Services implementation if the app contains that dependency.
+ impls.add(mPlayServicesProvider.getCameraDeviceSetupCompat(cameraId));
+ }
+ if (SDK_INT >= 35) {
+ try {
+ if (mCamera2Provider == null) {
+ // Create the camera2 implementation if there isn't a cached one.
+ mCamera2Provider = new Camera2CameraDeviceSetupCompatProvider(mContext);
+ }
+ impls.add(mCamera2Provider.getCameraDeviceSetupCompat(cameraId));
+ } catch (UnsupportedOperationException e) {
+ // This can throw UnsupportedOperationException for Android V upgrade devices. In
+ // that case, we treat it as SDK_INT < 35 and ignore.
+ }
+ }
+ return new AggregatedCameraDeviceSetupCompat(impls);
+ }
+
+ /**
+ * Returns a new instance of {@link CameraDeviceSetupCompatProvider}.
+ *
+ * @return The Play Services CameraDeviceSetupCompat implementation, or null if not found.
+ * @throws IllegalStateException if multiple Play Services CameraDeviceSetupCompat
+ * implementations are found, or failed to instantiate the
+ * implementation.
+ */
+ @Nullable
+ private CameraDeviceSetupCompatProvider getPlayServicesCameraDeviceSetupCompatProvider()
+ throws IllegalStateException {
+ PackageInfo packageInfo;
+ try {
+ packageInfo = mContext.getPackageManager().getPackageInfo(
+ mContext.getPackageName(),
+ PackageManager.GET_META_DATA | PackageManager.GET_SERVICES);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+
+ String playServiceImplClassName = null;
+ if (packageInfo.services == null) {
+ return null;
+ }
+ for (ServiceInfo serviceInfo : packageInfo.services) {
+ if (serviceInfo.metaData == null) {
+ continue;
+ }
+ // Try to load the play services impl class name from the Service metadata
+ // in camera-feature-combination-query-play-services manifest.
+ String className = serviceInfo.metaData.getString(PLAY_SERVICES_IMPL_KEY);
+ if (className != null) {
+ if (playServiceImplClassName != null) {
+ throw new IllegalStateException(
+ "Multiple Play Services CameraDeviceSetupCompat implementations"
+ + " found in the manifest.");
+ }
+ playServiceImplClassName = className;
+ }
+ }
+ if (playServiceImplClassName == null) {
+ // The app does not depend on a Play Services implementation.
+ return null;
+ }
+ return instantiatePlayServicesImplProvider(playServiceImplClassName);
+ }
+
+ /**
+ * Instantiates a Play Services CameraDeviceSetupCompat provider implementation.
+ *
+ * @param className The class name of the implementation.
+ * @return The instantiated implementation.
+ */
+ private CameraDeviceSetupCompatProvider instantiatePlayServicesImplProvider(
+ @NonNull String className) {
+ try {
+ Class<?> clazz = Class.forName(className);
+ return (CameraDeviceSetupCompatProvider) clazz.getConstructor(Context.class)
+ .newInstance(mContext);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Failed to instantiate Play Services CameraDeviceSetupCompat implementation",
+ e);
+ }
+ }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..b61d909
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import android.hardware.camera2.CameraAccessException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Interface for providing a {@link CameraDeviceSetupCompat} for a camera device.
+ *
+ * <p> Getting a provider usually needs Binder calls which is costly. This interface allows the
+ * to be cached and reused.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface CameraDeviceSetupCompatProvider {
+
+ /**
+ * Get a {@link CameraDeviceSetupCompat} for the camera device with the given cameraId.
+ *
+ * @param cameraId the cameraId of the camera device.
+ * @return a {@link CameraDeviceSetupCompat} for the camera device with the given cameraId.
+ */
+ @NonNull
+ CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+ throws CameraAccessException;
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java
new file mode 100644
index 0000000..cf686ce
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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.camera.featurecombinationquery;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import kotlin.RequiresOptIn;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Indicates that the annotated API uses the experimental feature combination query API.
+ *
+ * <p> This feature will move to official API in 1.5.0.
+ */
+@Retention(CLASS)
+@RequiresOptIn
+public @interface ExperimentalFeatureCombinationQuery {
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java
new file mode 100644
index 0000000..fdf1063
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@ExperimentalFeatureCombinationQuery
+package androidx.camera.featurecombinationquery;
+
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index 5e5ce6c..870cf0f 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -24,6 +24,8 @@
android {
namespace 'androidx.camera.integration.avsync'
+ compileSdk 35
+
defaultConfig {
applicationId "androidx.camera.integration.avsync"
}
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 453f43f..33c414d 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -21,6 +21,8 @@
}
android {
+ compileSdk 35
+
defaultConfig {
applicationId "androidx.camera.integration.uiwidgets"
versionCode 1
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 55f2e64e0..dfad750 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -127,5 +127,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.core"
}
diff --git a/compose/animation/animation-core/samples/build.gradle b/compose/animation/animation-core/samples/build.gradle
index f566a97..2a21612 100644
--- a/compose/animation/animation-core/samples/build.gradle
+++ b/compose/animation/animation-core/samples/build.gradle
@@ -51,5 +51,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.core.samples"
}
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index d384dff..3398b9f 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -121,5 +121,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.graphics"
}
diff --git a/compose/animation/animation-graphics/samples/build.gradle b/compose/animation/animation-graphics/samples/build.gradle
index c794e59..dd05f1e 100644
--- a/compose/animation/animation-graphics/samples/build.gradle
+++ b/compose/animation/animation-graphics/samples/build.gradle
@@ -52,5 +52,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.graphics.samples"
}
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 216eb4f..5e9f178 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -133,5 +133,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation"
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index a826c38..a146eb3 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -40,5 +40,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.demos"
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
index 7534afc..228b929 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
@@ -77,6 +77,7 @@
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import androidx.compose.ui.window.Popup
+import kotlin.collections.removeLast as removeLastKt
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.log
@@ -309,7 +310,7 @@
fun removeAnchor() {
if (anchors.size > 1) {
scope.launch { animatedOffset.snapTo(Offset.Zero) }
- anchors.removeLast()
+ anchors.removeLastKt()
modificationIndicator++
}
}
diff --git a/compose/animation/animation/samples/build.gradle b/compose/animation/animation/samples/build.gradle
index d7aecde..3d7e32c 100644
--- a/compose/animation/animation/samples/build.gradle
+++ b/compose/animation/animation/samples/build.gradle
@@ -50,5 +50,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.animation.samples"
}
diff --git a/compose/benchmark-utils/build.gradle b/compose/benchmark-utils/build.gradle
index 7e3d1ab..b994260 100644
--- a/compose/benchmark-utils/build.gradle
+++ b/compose/benchmark-utils/build.gradle
@@ -56,10 +56,11 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.benchmarkutils"
// workarounds for b/328649293
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index b68d2b4..cc742f9 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -119,5 +119,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.foundation.layout"
}
diff --git a/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle b/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
index e9a5acb..122b991 100644
--- a/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
+++ b/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
@@ -36,5 +36,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.foundation.layout.demos"
}
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index 8718492..7b33988 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -53,5 +53,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.foundation.layout.samples"
}
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 4544b8d..7a84faa 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -137,6 +137,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/foundation/foundation"
namespace "androidx.compose.foundation"
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
index 063a475..cc5cc22 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
@@ -47,5 +47,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.foundation.demos"
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
index 6e32f3d..504de76 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
@@ -72,6 +72,7 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlin.collections.removeFirst as removeFirstKt
import kotlin.math.roundToInt
import kotlin.math.roundToLong
import kotlin.system.measureNanoTime
@@ -453,7 +454,7 @@
fun addMeasure(duration: Long) {
values.add(duration)
while (values.size > capacity) {
- values.removeFirst()
+ values.removeFirstKt()
}
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
index aa3cf8b..4eb96a7 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
@@ -58,6 +58,7 @@
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
+import kotlin.collections.removeLast as removeLastKt
private val modifierKeys =
setOf(
@@ -100,7 +101,7 @@
if (keys.none { it.keyEvent.keyCode == event.keyCode && !it.isUp }) {
keys.add(0, KeyState(event))
if (keys.size > 10) {
- keys.removeLast()
+ keys.removeLastKt()
}
}
}
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 9308a13..a2e7565 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -55,6 +55,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.foundation.samples"
// TODO(b/328001575)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
index 182f9dd..0cf9953 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
@@ -42,6 +42,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import kotlin.collections.removeFirst as removeFirstKt
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
@@ -475,7 +476,7 @@
while (handles.isNotEmpty()) {
rule.runOnIdle {
assertThat(composed).contains(1)
- handles.removeFirst().release()
+ handles.removeFirstKt().release()
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
index c6f42a1..4db7891a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
@@ -51,6 +51,7 @@
class BasicTextHoverTest {
@get:Rule val rule = createComposeRule()
+ @Suppress("DEPRECATION")
@Test
fun whenSelectableText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runSelectableTest(
@@ -60,6 +61,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSelectableText_andSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runSelectableTest(
@@ -69,6 +71,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSelectableText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
runSelectableTest(
@@ -135,6 +138,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
fun whenNonSelectableText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runNonSelectableTest(
@@ -144,6 +148,7 @@
expectedTextIcon = TYPE_DEFAULT
)
+ @Suppress("DEPRECATION")
@Test
fun whenNonSelectableText_andSetIcon_inBoxWithDefaultIcon_setIconIsUsed() =
runNonSelectableTest(
@@ -153,6 +158,7 @@
expectedTextIcon = TYPE_CROSSHAIR
)
+ @Suppress("DEPRECATION")
@Test
fun whenNonSelectableText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
runNonSelectableTest(
@@ -217,6 +223,7 @@
}
}
+ @Suppress("DEPRECATION")
@Test
fun whenDisabledSelectionText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runDisabledSelectionText(
@@ -226,6 +233,7 @@
expectedTextIcon = TYPE_DEFAULT
)
+ @Suppress("DEPRECATION")
@Test
fun whenDisabledSelectionText_andSetIcon_inBoxWithDefaultIcon_setIconIsUsed() =
runDisabledSelectionText(
@@ -235,6 +243,7 @@
expectedTextIcon = TYPE_CROSSHAIR
)
+ @Suppress("DEPRECATION")
@Test
fun whenDisabledSelectionText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
runDisabledSelectionText(
@@ -337,6 +346,6 @@
// Exit hovering over element
rule.onNodeWithTag(selectionContainerTag).performMouseInput { exit() }
- assertIcon(TYPE_DEFAULT)
+ @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
index 5a27adf..7fd6761 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
@@ -52,6 +52,7 @@
class CoreTextFieldHoverTest {
@get:Rule val rule = createComposeRule()
+ @Suppress("DEPRECATION")
@Test
fun whenDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runTest(
@@ -61,6 +62,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runTest(
@@ -70,6 +72,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
runTest(
@@ -148,6 +151,6 @@
// Exit hovering over element
rule.onNodeWithTag(boxTag).performMouseInput { exit() }
- assertIcon(TYPE_DEFAULT)
+ @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
index ce78be4..d01acf3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
@@ -21,12 +21,13 @@
import android.view.inputmethod.ExtractedText
import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
import com.google.common.truth.Truth.assertThat
+import kotlin.collections.removeFirst as removeFirstKt
internal open class FakeInputMethodManager : ComposeInputMethodManager {
private val calls = mutableListOf<String>()
fun expectCall(description: String) {
- assertThat(calls.removeFirst()).isEqualTo(description)
+ assertThat(calls.removeFirstKt()).isEqualTo(description)
}
fun expectNoMoreCalls() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
index e873706..5c1cbcc 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
@@ -51,6 +51,7 @@
class BasicTextFieldHoverTest {
@get:Rule val rule = createComposeRule()
+ @Suppress("DEPRECATION")
@Test
fun whenDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runTest(
@@ -60,6 +61,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
runTest(
@@ -69,6 +71,7 @@
expectedTextIcon = TYPE_TEXT
)
+ @Suppress("DEPRECATION")
@Test
fun whenSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
runTest(
@@ -146,6 +149,6 @@
// Exit hovering over element
rule.onNodeWithTag(boxTag).performMouseInput { exit() }
- assertIcon(TYPE_DEFAULT)
+ @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
index bebdcbd..a1e4a61 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
@@ -56,7 +56,7 @@
override fun actualCustomStringRepresentation(): String {
return if (subject != null) {
- "($subject ${subject.width}x${subject.height} ${subject.config.name})"
+ "($subject ${subject.width}x${subject.height} ${subject.config!!.name})"
} else {
super.actualCustomStringRepresentation()
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
index b54b8b8..1c50185 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
@@ -20,6 +20,8 @@
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.util.fastForEach
+import kotlin.collections.removeFirst as removeFirstKt
+import kotlin.collections.removeLast as removeLastKt
/**
* A generic purpose undo/redo stack manager.
@@ -60,7 +62,7 @@
redoStack.clear()
while (size > capacity - 1) { // leave room for the immediate `add`
- undoStack.removeFirst()
+ undoStack.removeFirstKt()
}
undoStack.add(undoableAction)
}
@@ -77,7 +79,7 @@
"Please first check `canUndo` value before calling the `undo` function."
}
- val topOperation = undoStack.removeLast()
+ val topOperation = undoStack.removeLastKt()
redoStack.add(topOperation)
return topOperation
@@ -95,7 +97,7 @@
"Please first check `canRedo` value before calling the `redo` function."
}
- val topOperation = redoStack.removeLast()
+ val topOperation = redoStack.removeLastKt()
undoStack.add(topOperation)
return topOperation
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 7d2fed8..8622500 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -46,5 +46,6 @@
ApkCopyHelperKt.setupAppApkCopy(project, "release")
android {
+ compileSdk 35
namespace "androidx.compose.integration.demos"
}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 5e67574..a663c70 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -161,7 +161,9 @@
SideEffect {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !isDarkMode
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !isDarkMode
+ @Suppress("deprecation")
window.statusBarColor = Color.Transparent.toArgb()
+ @Suppress("deprecation")
window.navigationBarColor = Color.Transparent.toArgb()
}
MaterialTheme(colorScheme = colorScheme, content = content)
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index e3ff228..7b9584b 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -70,6 +70,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.integration.docs"
}
diff --git a/compose/integration-tests/hero/hero-implementation/build.gradle b/compose/integration-tests/hero/hero-implementation/build.gradle
index 690088e..2543f87 100644
--- a/compose/integration-tests/hero/hero-implementation/build.gradle
+++ b/compose/integration-tests/hero/hero-implementation/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdk 35
namespace 'androidx.compose.integration.hero.implementation'
lint {
@@ -59,4 +60,4 @@
implementation(project(":compose:runtime:runtime-tracing"))
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-tooling"))
-}
\ No newline at end of file
+}
diff --git a/compose/integration-tests/hero/macrobenchmark-target/build.gradle b/compose/integration-tests/hero/macrobenchmark-target/build.gradle
index 181d642..acb2d78b 100644
--- a/compose/integration-tests/hero/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/hero/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.compose.integration.hero.macrobenchmark.target"
buildTypes {
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 5946d76..a4cf022 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -13,6 +13,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.integration.macrobenchmark.target"
buildTypes {
release {
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index 973f875..31e34a8 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -32,6 +32,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.compose.material.catalog"
versionCode 2400
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index 0ce9ddb..26c903c 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -48,6 +48,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.navigation"
// TODO(b/349411310)?
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/material/material-navigation/samples/build.gradle b/compose/material/material-navigation/samples/build.gradle
index b0a9deb..20b3d14d 100644
--- a/compose/material/material-navigation/samples/build.gradle
+++ b/compose/material/material-navigation/samples/build.gradle
@@ -48,5 +48,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.navigation.samples"
}
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 77af4eb..2fa1de4 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -113,5 +113,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.ripple"
}
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 2a7e5d6..724fa3f 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -142,6 +142,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/material/material"
namespace "androidx.compose.material"
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index ba6ac0a..31c9cef 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -49,5 +49,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.catalog.library"
}
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index c51c55c..a9bb660 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -37,5 +37,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.demos"
}
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
index 04dac12..69dc9d9 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
@@ -64,7 +64,9 @@
setContent {
val palette = interpolateTheme(scrollFraction.floatValue)
val darkenedPrimary = palette.darkenedPrimary
+ @Suppress("DEPRECATION")
window.statusBarColor = darkenedPrimary
+ @Suppress("DEPRECATION")
window.navigationBarColor = darkenedPrimary
DynamicThemeApp(scrollFraction, palette)
diff --git a/compose/material/material/samples/build.gradle b/compose/material/material/samples/build.gradle
index 4f48d58..8ec0f0d 100644
--- a/compose/material/material/samples/build.gradle
+++ b/compose/material/material/samples/build.gradle
@@ -54,6 +54,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material.samples"
// TODO(b/328001575)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index bc2a2d1..e4d441b 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -101,10 +101,6 @@
}
}
-android {
- namespace "androidx.compose.material3.adaptive.layout"
-}
-
androidx {
name = "Material Adaptive"
type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
@@ -120,6 +116,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace "androidx.compose.material3.adaptive.layout"
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index 9c75325..001bd0a 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -114,6 +114,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace "androidx.compose.material3.adaptive.navigation"
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
index 10a5006..3feed63 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
@@ -42,6 +42,7 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastMap
+import kotlin.collections.removeLast as removeLastKt
/**
* The common interface of the default navigation implementations for different three-pane
@@ -344,7 +345,7 @@
}
val targetSize = previousDestinationIndex + 1
while (destinationHistory.size > targetSize) {
- destinationHistory.removeLast()
+ destinationHistory.removeLastKt()
}
return true
}
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 2289202..ecc8e46 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -114,6 +114,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace "androidx.compose.material3.adaptive"
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
index 18b0996..bf9047b 100644
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
+++ b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
@@ -83,7 +83,10 @@
internal class MockWindowMetricsCalculator(private val mockWindowSize: State<IntSize>) :
WindowMetricsCalculator {
override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
- return WindowMetrics(Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height))
+ return WindowMetrics(
+ Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
+ density = 1f
+ )
}
override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
@@ -91,6 +94,9 @@
}
override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
- return WindowMetrics(Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height))
+ return WindowMetrics(
+ Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
+ density = 1f
+ )
}
}
diff --git a/compose/material3/integration-tests/macrobenchmark-target/build.gradle b/compose/material3/integration-tests/macrobenchmark-target/build.gradle
index 087ceb2..c19b92d 100644
--- a/compose/material3/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/material3/integration-tests/macrobenchmark-target/build.gradle
@@ -14,9 +14,10 @@
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+ compileSdkVersion 35
}
dependencies {
implementation(project(":activity:activity-compose"))
implementation(project(":compose:material3:material3"))
-}
\ No newline at end of file
+}
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index 83d23c0..62a3e0d 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -59,10 +59,12 @@
}
public static final class NavigationSuiteType.Companion {
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public String getCustom();
method public String getNavigationBar();
method public String getNavigationDrawer();
method public String getNavigationRail();
method public String getNone();
+ property @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final String Custom;
property public final String NavigationBar;
property public final String NavigationDrawer;
property public final String NavigationRail;
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index 83d23c0..62a3e0d 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -59,10 +59,12 @@
}
public static final class NavigationSuiteType.Companion {
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public String getCustom();
method public String getNavigationBar();
method public String getNavigationDrawer();
method public String getNavigationRail();
method public String getNone();
+ property @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final String Custom;
property public final String NavigationBar;
property public final String NavigationDrawer;
property public final String NavigationRail;
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index 83a6e86..1f731d6 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -96,6 +96,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material3.adaptive.navigationsuite"
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index de80399..5e96cd5 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -381,6 +381,16 @@
val NavigationDrawer = NavigationSuiteType(description = "NavigationDrawer")
/**
+ * A navigation suite type that instructs the [NavigationSuite] to perform specialized
+ * custom behavior. Different `NavigationSuite` implementations will exhibit different
+ * behaviors when using this type.
+ */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3AdaptiveNavigationSuiteApi
+ @ExperimentalMaterial3AdaptiveNavigationSuiteApi
+ val Custom = NavigationSuiteType(description = "Custom")
+
+ /**
* A navigation suite type that instructs the [NavigationSuite] to not display any
* navigation components on the screen.
*/
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 286e981..c488a59 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -90,6 +90,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material3.common"
}
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index e415e38..f9109995 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -104,5 +104,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material3.windowsizeclass"
}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 333fdf4..9b09714 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -129,6 +129,7 @@
// Screenshot tests related setup
android {
+ compileSdk 35
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/material3/material3"
namespace "androidx.compose.material3"
diff --git a/compose/material3/material3/integration-tests/material3-demos/build.gradle b/compose/material3/material3/integration-tests/material3-demos/build.gradle
index 4902826..539b8cb 100644
--- a/compose/material3/material3/integration-tests/material3-demos/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-demos/build.gradle
@@ -55,5 +55,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.material3.demos"
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 624415c..61654d2 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -567,6 +567,7 @@
latch.countDown()
}
+ @Deprecated("deprecated")
override fun onLowMemory() {
// NO-OP
}
@@ -622,6 +623,7 @@
latch.countDown()
}
+ @Deprecated("deprecated")
override fun onLowMemory() {
// NO-OP
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 5a06bfd..eadf4b8 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -209,6 +209,7 @@
latch.countDown()
}
+ @Deprecated("deprecated")
override fun onLowMemory() {
// NO-OP
}
@@ -265,6 +266,7 @@
latch.countDown()
}
+ @Deprecated("deprecated")
override fun onLowMemory() {
// NO-OP
}
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index 45c128e..98fdb63 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -114,5 +114,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.runtime.saveable"
}
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index 588e594..5038a00 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -93,6 +93,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.runtime.integrationtests"
}
@@ -177,4 +178,4 @@
task.source = findFile()
task.sizes = project.findProperty("compose.newExpectedSizes")
}
-}
\ No newline at end of file
+}
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 7745525..1fe35c8 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -107,5 +107,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.testutils"
}
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
index f96bd8a..680479b 100644
--- a/compose/ui/ui-graphics/benchmark/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -34,5 +34,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.graphics.benchmark"
}
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index d761f6d..202ab22 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -123,5 +123,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.graphics"
}
diff --git a/compose/ui/ui-graphics/samples/build.gradle b/compose/ui/ui-graphics/samples/build.gradle
index 2540cfd..927431d 100644
--- a/compose/ui/ui-graphics/samples/build.gradle
+++ b/compose/ui/ui-graphics/samples/build.gradle
@@ -50,5 +50,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.graphics.samples"
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index 2de13ab..ed2e9d0 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -62,6 +62,7 @@
// NO-OP
}
+ @Suppress("OVERRIDE_DEPRECATION")
override fun onLowMemory() {
// NO-OP
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
index 91da4b4..93f415a 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
@@ -66,7 +66,7 @@
get() = bitmap.height
override val config: ImageBitmapConfig
- get() = bitmap.config.toImageConfig()
+ get() = bitmap.config!!.toImageConfig()
override val colorSpace: ColorSpace
get() =
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 33019de..fbd989f 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -214,7 +214,7 @@
override fun getBounds(): Rect {
if (rectF == null) rectF = PlatformRectF()
with(rectF!!) {
- internalPath.computeBounds(this, true)
+ @Suppress("DEPRECATION") internalPath.computeBounds(this, true)
return Rect(this.left, this.top, this.right, this.bottom)
}
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
index 4c3e7ca..f0758b9 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.graphics
import androidx.annotation.RestrictTo
+import kotlin.collections.removeLast as removeLastKt
import kotlin.math.max
import kotlin.math.min
@@ -152,7 +153,11 @@
val s = stack
s.add(root)
while (s.size > 0) {
- val node = s.removeLast()
+
+ // MutableCollections.removeLast() is shadowed by java.util.list.removeAt()
+ // which was added in sdk 35 making this call unsafe
+ // val node = s.removeLast()
+ val node = s.removeLastKt()
if (node.overlaps(start, end)) block(node)
if (node.left !== terminator && node.left.max >= start) {
s.add(node.left)
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 20b1101..b7d7bbd 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -80,6 +80,7 @@
}
android {
+ compileSdk 35
defaultConfig {
// layout inspection supported starting on Android Q
minSdkVersion 29
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
index b38bed3..e5a9a65 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -52,6 +52,7 @@
import androidx.inspection.InspectorFactory
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
+import kotlin.collections.removeLast as removeLastKt
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersResponse
@@ -97,7 +98,7 @@
val stack = mutableListOf<InspectorNode>()
trees.forEach { stack.addAll(it.nodes) }
while (stack.isNotEmpty()) {
- val node = stack.removeLast()
+ val node = stack.removeLastKt()
stack.addAll(node.children)
result.put(node.id, node)
}
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 7bb5e9b..46b766d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -112,6 +112,7 @@
android {
+ compileSdk 35
lintOptions {
disable("InvalidPackage")
}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
index 4fd6efb..387b078 100644
--- a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
@@ -33,5 +33,6 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.compose.ui.test.manifest.integration.testapp"
}
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index b780393..e5c600c 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -135,6 +135,7 @@
}
android {
+ compileSdk 35
sourceSets {
test.java.srcDirs += "src/androidCommonTest/kotlin"
androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
diff --git a/compose/ui/ui-test/samples/build.gradle b/compose/ui/ui-test/samples/build.gradle
index 2f926bc..7567c76 100644
--- a/compose/ui/ui-test/samples/build.gradle
+++ b/compose/ui/ui-test/samples/build.gradle
@@ -51,5 +51,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.test.samples"
}
diff --git a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
index 5670ba3..de21d68 100644
--- a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
+++ b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.test.util.verifyMouseEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth
+import kotlin.collections.removeFirst as removeFirstKt
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -447,7 +448,7 @@
}
private fun <E> MutableList<E>.removeFirst(n: Int): List<E> {
- return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirst()) } }
+ return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirstKt()) } }
}
private fun enqueueKeyPress(key: Key) {
diff --git a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
index c8bac18..829d278 100644
--- a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
+++ b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
@@ -45,6 +45,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlin.collections.removeFirst as removeFirstKt
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@@ -815,6 +816,6 @@
}
private fun <E> MutableList<E>.removeFirst(n: Int): List<E> {
- return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirst()) } }
+ return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirstKt()) } }
}
}
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
index 06afb53..10747c9 100644
--- a/compose/ui/ui-text-google-fonts/build.gradle
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -56,5 +56,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.text.googlefonts"
}
diff --git a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
index 9789e31..8859e04 100644
--- a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
+++ b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
@@ -81,7 +81,7 @@
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
val packageInfo: PackageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
- @Suppress("DEPRECATION") return convertToByteArrayList(packageInfo.signatures)
+ @Suppress("DEPRECATION") return convertToByteArrayList(packageInfo.signatures!!)
}
private val ByteArrayComparator = Comparator { l: ByteArray, r: ByteArray ->
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index bf6e10a..8bfa343 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -39,5 +39,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.text.benchmark"
}
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 3799ef5..4dd85da 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -140,6 +140,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.text"
// TODO(b/328001575)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-text/samples/build.gradle b/compose/ui/ui-text/samples/build.gradle
index b6d1240..f19e8a0 100644
--- a/compose/ui/ui-text/samples/build.gradle
+++ b/compose/ui/ui-text/samples/build.gradle
@@ -51,6 +51,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.text.samples"
// TODO(b/328001575)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
index 9f676b8..617f75c 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
@@ -56,7 +56,7 @@
override fun actualCustomStringRepresentation(): String {
return if (subject != null) {
- "($subject ${subject.width}x${subject.height} ${subject.config.name})"
+ "($subject ${subject.width}x${subject.height} ${subject.config!!.name})"
} else {
super.actualCustomStringRepresentation()
}
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index d7cd9ff..be053d0 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -109,5 +109,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.tooling.data"
}
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index d72f2c9..c824c3e 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -118,6 +118,7 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.tooling"
// TODO(b/345531033)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
index 958368d..d373115 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.tooling.data.Group
import androidx.compose.ui.tooling.data.UiToolingDataApi
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import kotlin.collections.removeLast as removeLastKt
/** Tries to find the [Class] of the [PreviewParameterProvider] corresponding to the given FQN. */
internal fun String.asPreviewProviderClass(): Class<out PreviewParameterProvider<*>>? {
@@ -119,7 +120,7 @@
val result = mutableListOf<Group>()
val stack = mutableListOf(root)
while (stack.isNotEmpty()) {
- val current = stack.removeLast()
+ val current = stack.removeLastKt()
if (predicate(current)) {
if (findOnlyFirst) {
return listOf(current)
diff --git a/compose/ui/ui-unit/samples/build.gradle b/compose/ui/ui-unit/samples/build.gradle
index ae1cb6e..42100f5 100644
--- a/compose/ui/ui-unit/samples/build.gradle
+++ b/compose/ui/ui-unit/samples/build.gradle
@@ -51,5 +51,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.unit.samples"
}
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index 5e60265..9070854 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -59,5 +59,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.viewbinding"
}
diff --git a/compose/ui/ui-viewbinding/samples/build.gradle b/compose/ui/ui-viewbinding/samples/build.gradle
index c2e2d2c..05dbaa8 100644
--- a/compose/ui/ui-viewbinding/samples/build.gradle
+++ b/compose/ui/ui-viewbinding/samples/build.gradle
@@ -58,6 +58,7 @@
}
android {
+ compileSdk 35
buildFeatures {
viewBinding true
}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 02f2ad8a..765fd5a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -191,6 +191,10 @@
public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
}
+ public final class SensitiveContentKt {
+ method public static androidx.compose.ui.Modifier sensitiveContent(androidx.compose.ui.Modifier, optional boolean isContentSensitive);
+ }
+
@androidx.compose.runtime.ComposableTargetMarker(description="UI Composable") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.TYPE_PARAMETER}) public @interface UiComposable {
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d7fbf23..b89aeb3 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -191,6 +191,10 @@
public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
}
+ public final class SensitiveContentKt {
+ method public static androidx.compose.ui.Modifier sensitiveContent(androidx.compose.ui.Modifier, optional boolean isContentSensitive);
+ }
+
@androidx.compose.runtime.ComposableTargetMarker(description="UI Composable") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.TYPE_PARAMETER}) public @interface UiComposable {
}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 0d5ceba..ce4b1c2 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -205,6 +205,7 @@
}
android {
+ compileSdk 35
testOptions.unitTests.includeAndroidResources = true
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
diff --git a/compose/ui/ui/integration-tests/ui-demos/build.gradle b/compose/ui/ui/integration-tests/ui-demos/build.gradle
index 4c94cac..70d9088 100644
--- a/compose/ui/ui/integration-tests/ui-demos/build.gradle
+++ b/compose/ui/ui/integration-tests/ui-demos/build.gradle
@@ -43,6 +43,7 @@
}
android {
+ compileSdk 35
buildFeatures {
viewBinding true
}
diff --git a/compose/ui/ui/samples/build.gradle b/compose/ui/ui/samples/build.gradle
index ffe2ad7..ed4179a 100644
--- a/compose/ui/ui/samples/build.gradle
+++ b/compose/ui/ui/samples/build.gradle
@@ -53,5 +53,6 @@
}
android {
+ compileSdk 35
namespace "androidx.compose.ui.samples"
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt
new file mode 100644
index 0000000..74c3f89
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2024 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
+
+import android.os.Build
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.draw.Row
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(35)
+class SensitiveContentModifierTest {
+ @get:Rule val rule = createComposeRule()
+ private lateinit var androidComposeView: View
+
+ @Before
+ fun before() {
+ isDebugInspectorInfoEnabled = true
+ Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ }
+
+ @After
+ fun after() {
+ isDebugInspectorInfoEnabled = false
+ }
+
+ @Test
+ fun assertModifierEquals() {
+ val modifier = Modifier.sensitiveContent()
+ Assert.assertEquals(modifier, modifier)
+ Assert.assertEquals(modifier, Modifier.sensitiveContent())
+ Assert.assertNotEquals(modifier, Modifier.sensitiveContent(false))
+ }
+
+ @Test
+ fun testSensitiveContent() {
+ rule.setContent {
+ Text("User name", modifier = Modifier.sensitiveContent())
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testNonSensitiveContent() {
+ rule.setContent {
+ Text("Hello world!")
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testDynamicSensitiveModifier() {
+ var isContentSensitive by mutableStateOf(true)
+ rule.setContent {
+ Text(
+ "Dynamic sensitive Content",
+ modifier = Modifier.sensitiveContent(isContentSensitive)
+ )
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // update modifier as non sensitive and verify
+ rule.runOnIdle { isContentSensitive = false }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testMultipleSensitiveComposable() {
+ var isContentSensitive by mutableStateOf(true)
+ var isContentSensitive2 by mutableStateOf(true)
+ rule.setContent {
+ Row {
+ Text(
+ "Dynamic sensitive Content",
+ modifier = if (isContentSensitive) Modifier.sensitiveContent() else Modifier
+ )
+ Text(
+ "Dynamic sensitive Content",
+ modifier = if (isContentSensitive2) Modifier.sensitiveContent() else Modifier
+ )
+ }
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // mark one composable non sensitive and verify
+ rule.runOnIdle { isContentSensitive = false }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // mark another composable non sensitive and verify
+ rule.runOnIdle { isContentSensitive2 = false }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // mark one composable sensitive again and verify
+ rule.runOnIdle { isContentSensitive2 = true }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testRemoveSensitiveModifier() {
+ var isModifierIncluded by mutableStateOf(true)
+ rule.setContent {
+ Text(
+ "Dynamic sensitive Content",
+ modifier = if (isModifierIncluded) Modifier.sensitiveContent() else Modifier
+ )
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // remove sensitive modifier from composable and verify
+ rule.runOnIdle { isModifierIncluded = false }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testAddSensitiveModifier() {
+ var isModifierIncluded by mutableStateOf(false)
+ rule.setContent {
+ Text(
+ "Dynamic sensitive Content",
+ modifier = if (isModifierIncluded) Modifier.sensitiveContent() else Modifier
+ )
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // add sensitive modifier to the composable and verify
+ rule.runOnIdle { isModifierIncluded = true }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testRemoveSensitiveComposable() {
+ var isSensitiveComposableIncluded by mutableStateOf(true)
+ rule.setContent {
+ if (isSensitiveComposableIncluded) {
+ Text("Dynamic sensitive Content", modifier = Modifier.sensitiveContent())
+ }
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // remove sensitive composable and verify
+ rule.runOnIdle { isSensitiveComposableIncluded = false }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+
+ @Test
+ fun testAddSensitiveComposable() {
+ var isSensitiveComposableIncluded by mutableStateOf(false)
+ rule.setContent {
+ if (isSensitiveComposableIncluded) {
+ Text("Dynamic sensitive Content", modifier = Modifier.sensitiveContent())
+ }
+ androidComposeView = LocalView.current
+ }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_AUTO,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+
+ // add sensitive composable and verify
+ rule.runOnIdle { isSensitiveComposableIncluded = true }
+ rule.runOnIdle {
+ Assert.assertEquals(
+ View.CONTENT_SENSITIVITY_SENSITIVE,
+ androidComposeView.getContentSensitivity()
+ )
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 893f08a..1d0476c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -1152,6 +1152,7 @@
latch?.countDown()
}
+ @Deprecated("This callback is superseded by onTrimMemory")
override fun onLowMemory() {
// NO-OP
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
index b884769..d5169ed 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
@@ -68,6 +68,7 @@
/** Creates [PointerIcon] from pointer icon type (see [android.view.PointerIcon.getSystemIcon] */
fun PointerIcon(pointerIconType: Int): PointerIcon = AndroidPointerIconType(pointerIconType)
+@Suppress("DEPRECATION")
internal actual val pointerIconDefault: PointerIcon = AndroidPointerIconType(TYPE_DEFAULT)
internal actual val pointerIconCrosshair: PointerIcon = AndroidPointerIconType(TYPE_CROSSHAIR)
internal actual val pointerIconText: PointerIcon = AndroidPointerIconType(TYPE_TEXT)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 8e3e45e..da494eb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -2438,6 +2438,27 @@
override fun shouldDelayChildPressedState(): Boolean = false
+ // Track sensitive composable visible in this view
+ private var sensitiveComponentCount = 0
+
+ override fun incrementSensitiveComponentCount() {
+ if (SDK_INT >= 35) {
+ if (sensitiveComponentCount == 0) {
+ AndroidComposeViewSensitiveContent35.setContentSensitivity(view, true)
+ }
+ sensitiveComponentCount += 1
+ }
+ }
+
+ override fun decrementSensitiveComponentCount() {
+ if (SDK_INT >= 35) {
+ if (sensitiveComponentCount == 1) {
+ AndroidComposeViewSensitiveContent35.setContentSensitivity(view, false)
+ }
+ sensitiveComponentCount -= 1
+ }
+ }
+
companion object {
private var systemPropertiesClass: Class<*>? = null
private var getBooleanMethod: Method? = null
@@ -2621,6 +2642,19 @@
fun calculateMatrixToWindow(view: View, matrix: Matrix)
}
+@RequiresApi(35)
+private object AndroidComposeViewSensitiveContent35 {
+ @DoNotInline
+ @RequiresApi(35)
+ fun setContentSensitivity(view: View, isSensitiveContent: Boolean) {
+ if (isSensitiveContent) {
+ view.setContentSensitivity(View.CONTENT_SENSITIVITY_SENSITIVE)
+ } else {
+ view.setContentSensitivity(View.CONTENT_SENSITIVITY_AUTO)
+ }
+ }
+}
+
@RequiresApi(Q)
private class CalculateMatrixToWindowApi29 : CalculateMatrixToWindow {
private val tmpMatrix = android.graphics.Matrix()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
index b865c80..fd0963c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
@@ -126,6 +126,7 @@
resourceIdCache.clear()
}
+ @Deprecated("This callback is superseded by onTrimMemory")
override fun onLowMemory() {
resourceIdCache.clear()
}
@@ -160,6 +161,7 @@
currentConfiguration.setTo(configuration)
}
+ @Deprecated("This callback is superseded by onTrimMemory")
override fun onLowMemory() {
imageVectorCache.clear()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt
new file mode 100644
index 0000000..df250df
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 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
+
+import androidx.compose.ui.internal.checkPrecondition
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * This modifier hints that the composable renders sensitive content (i.e. username, password,
+ * credit card etc) on the screen, and the content should be protected during screen share in
+ * supported environments.
+ */
+fun Modifier.sensitiveContent(isContentSensitive: Boolean = true): Modifier =
+ this then SensitiveNodeElement(isContentSensitive)
+
+private data class SensitiveNodeElement(val isContentSensitive: Boolean) :
+ ModifierNodeElement<SensitiveContentNode>() {
+ override fun create(): SensitiveContentNode = SensitiveContentNode(isContentSensitive)
+
+ override fun update(node: SensitiveContentNode) {
+ node.isContentSensitive = isContentSensitive
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "sensitiveContent"
+ properties["isContentSensitive"] = isContentSensitive
+ }
+}
+
+private data class SensitiveContentNode(private var _isContentSensitive: Boolean) :
+ Modifier.Node() {
+ // Tracks if this node has been counted as sensitive or not.
+ private var isCountedSensitive: Boolean = false
+
+ var isContentSensitive: Boolean = _isContentSensitive
+ set(value) {
+ field = value
+ if (isContentSensitive && !isCountedSensitive) {
+ requireOwner().incrementSensitiveComponentCount()
+ isCountedSensitive = true
+ } else if (!isContentSensitive && isCountedSensitive) {
+ requireOwner().decrementSensitiveComponentCount()
+ isCountedSensitive = false
+ }
+ }
+
+ override fun onAttach() {
+ super.onAttach()
+ if (isContentSensitive) {
+ checkPrecondition(!isCountedSensitive) { "invalid sensitive content state" }
+ requireOwner().incrementSensitiveComponentCount()
+ isCountedSensitive = true
+ }
+ }
+
+ override fun onDetach() {
+ if (isCountedSensitive) {
+ requireOwner().decrementSensitiveComponentCount()
+ isCountedSensitive = false
+ }
+ super.onDetach()
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 1f93d1c..30d11e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -317,6 +317,20 @@
session: suspend PlatformTextInputSessionScope.() -> Nothing
): Nothing
+ /**
+ * Tracks sensitive content on the screen to protect user privacy. Increment sensitive component
+ * count by 1. Implementation may protect user privacy by not showing sensitive content
+ * (username, password etc) to remote viewer during screen share.
+ */
+ fun incrementSensitiveComponentCount() {}
+
+ /**
+ * Tracks sensitive content on the screen to protect user privacy. Decrement sensitive component
+ * count by 1. Implementation may protect user privacy by not showing sensitive content
+ * (username, password etc) to remote viewer during screen share.
+ */
+ fun decrementSensitiveComponentCount() {}
+
companion object {
/**
* Enables additional (and expensive to do in production) assertions. Useful to be set to
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 4412f9d..5b810b4 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -114,5 +114,6 @@
}
android {
+ compileSdk 35
namespace "androidx.constraintlayout.compose"
}
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
index db7fe29..5afedb3 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
@@ -35,6 +35,7 @@
}
android {
+ compileSdk 35
namespace "androidx.constraintlayout.compose.demos"
}
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
index 7b273f9..22a5142 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
minSdkVersion 26
}
@@ -60,4 +61,4 @@
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-tooling"))
implementation(project(":profileinstaller:profileinstaller"))
-}
\ No newline at end of file
+}
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index ea79373..10e75e1 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -21,6 +21,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.core.graphics.sample"
}
diff --git a/core/core-ktx/build.gradle b/core/core-ktx/build.gradle
index f637c66..e71f40e 100644
--- a/core/core-ktx/build.gradle
+++ b/core/core-ktx/build.gradle
@@ -43,5 +43,6 @@
}
android {
+ compileSdk 35
namespace "androidx.core.ktx"
}
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
index 0cd76e1..1aa472a 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
@@ -60,7 +60,7 @@
val p = r1 + r2
val r = RectF()
- p.computeBounds(r, true)
+ @Suppress("DEPRECATION") p.computeBounds(r, true)
assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
}
@@ -72,7 +72,7 @@
val p = r1 or r2
val r = RectF()
- p.computeBounds(r, true)
+ @Suppress("DEPRECATION") p.computeBounds(r, true)
assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
}
@@ -84,7 +84,7 @@
val p = r1 - r2
val r = RectF()
- p.computeBounds(r, true)
+ @Suppress("DEPRECATION") p.computeBounds(r, true)
assertEquals(RectF(0.0f, 0.0f, 5.0f, 10.0f), r)
}
@@ -96,7 +96,7 @@
val p = r1 and r2
val r = RectF()
- p.computeBounds(r, true)
+ @Suppress("DEPRECATION") p.computeBounds(r, true)
assertEquals(RectF(5.0f, 0.0f, 10.0f, 10.0f), r)
}
@@ -117,7 +117,7 @@
val p = r1 xor r2
val r = RectF()
- p.computeBounds(r, true)
+ @Suppress("DEPRECATION") p.computeBounds(r, true)
assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
}
diff --git a/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt b/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
index 2577ae6..915b7a2 100644
--- a/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
+++ b/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
@@ -21,6 +21,7 @@
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Px
+import kotlin.collections.removeLast as removeLastKt
/**
* Returns the view at [index].
@@ -170,7 +171,10 @@
} else {
while (!iterator.hasNext() && stack.isNotEmpty()) {
iterator = stack.last()
- stack.removeLast()
+ // MutableCollections.removeLast() is shadowed by java.util.list.removeAt()
+ // which was added in sdk 35 making this call unsafe
+ // stack.removeLast()
+ stack.removeLastKt()
}
}
}
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 73c9942..4fc65ad 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -31,6 +31,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.core.splashscreen.samples"
}
diff --git a/core/core-testing/build.gradle b/core/core-testing/build.gradle
index ce43285..053eb22 100644
--- a/core/core-testing/build.gradle
+++ b/core/core-testing/build.gradle
@@ -44,6 +44,7 @@
}
android {
+ compileSdk 35
namespace "androidx.core.testing"
}
diff --git a/core/core/build.gradle b/core/core/build.gradle
index d4cac38..0b8acc6 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -71,6 +71,7 @@
}
android {
+ compileSdk = 35
buildFeatures {
aidl = true
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
index d213e1f..70bf03c 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.os.Bundle;
import android.os.Parcel;
import android.support.v4.BaseInstrumentationTestCase;
import android.text.SpannableStringBuilder;
@@ -91,6 +92,8 @@
@Test
public void testSetStylusHandwritingEnabled() {
EditorInfo editorInfo = new EditorInfo();
+ assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true /* enabled */);
assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
@@ -99,6 +102,45 @@
}
@Test
+ public void testSetStylusHandwritingEnabled_compatWithCoreVersion1_13() {
+ EditorInfo editorInfo = new EditorInfo();
+ setStylusHandwritingEnabled_coreVersion1_13(editorInfo, false);
+ assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+ editorInfo = new EditorInfo();
+ setStylusHandwritingEnabled_coreVersion1_13(editorInfo, true);
+ assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+ editorInfo = new EditorInfo();
+ EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
+ assertFalse(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
+
+ editorInfo = new EditorInfo();
+ EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
+ assertTrue(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 35)
+ public void testSetStylusHandwritingEnabled_compatWithAndroidV() {
+ EditorInfo editorInfo = new EditorInfo();
+ editorInfo.setStylusHandwritingEnabled(false);
+ assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+ editorInfo = new EditorInfo();
+ editorInfo.setStylusHandwritingEnabled(true);
+ assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+ editorInfo = new EditorInfo();
+ EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
+ assertFalse(editorInfo.isStylusHandwritingEnabled());
+
+ editorInfo = new EditorInfo();
+ EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
+ assertTrue(editorInfo.isStylusHandwritingEnabled());
+ }
+
+ @Test
public void setInitialText_nullInputText_throwsException() {
final CharSequence testText = null;
final EditorInfo editorInfo = new EditorInfo();
@@ -319,4 +361,22 @@
}
return builder;
}
+
+ /** This is the version in AndroidX Core library 1.13. */
+ private static void setStylusHandwritingEnabled_coreVersion1_13(
+ EditorInfo editorInfo, boolean enabled) {
+ if (editorInfo.extras == null) {
+ editorInfo.extras = new Bundle();
+ }
+ editorInfo.extras.putBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY, enabled);
+ }
+
+ /** This is the version in AndroidX Core library 1.13. */
+ public static boolean isStylusHandwritingEnabled_coreVersion1_13(EditorInfo editorInfo) {
+ if (editorInfo.extras == null) {
+ // disabled by default
+ return false;
+ }
+ return editorInfo.extras.getBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY);
+ }
}
diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
index 7ef503a..0686848 100644
--- a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
@@ -41,6 +41,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
@@ -104,7 +105,8 @@
private static final String CONTENT_SELECTION_END_KEY =
"androidx.core.view.inputmethod.EditorInfoCompat.CONTENT_SELECTION_END";
- private static final String STYLUS_HANDWRITING_ENABLED_KEY =
+ @VisibleForTesting
+ static final String STYLUS_HANDWRITING_ENABLED_KEY =
"androidx.core.view.inputmethod.EditorInfoCompat.STYLUS_HANDWRITING_ENABLED";
@Retention(SOURCE)
@@ -209,6 +211,9 @@
*/
public static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo,
boolean enabled) {
+ if (BuildCompat.isAtLeastV()) {
+ Api35Impl.setStylusHandwritingEnabled(editorInfo, enabled);
+ }
if (editorInfo.extras == null) {
editorInfo.extras = new Bundle();
}
@@ -222,11 +227,14 @@
* @see InputMethodManager#isStylusHandwritingAvailable()
*/
public static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
- if (editorInfo.extras == null) {
- // disabled by default
- return false;
+ if (editorInfo.extras != null
+ && editorInfo.extras.containsKey(STYLUS_HANDWRITING_ENABLED_KEY)) {
+ return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
}
- return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
+ if (BuildCompat.isAtLeastV()) {
+ return Api35Impl.isStylusHandwritingEnabled(editorInfo);
+ }
+ return false;
}
/**
@@ -589,4 +597,17 @@
return editorInfo.getInitialTextAfterCursor(length, flags);
}
}
+
+ @RequiresApi(35)
+ private static class Api35Impl {
+ private Api35Impl() {}
+
+ static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo, boolean enabled) {
+ editorInfo.setStylusHandwritingEnabled(enabled);
+ }
+
+ static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
+ return editorInfo.isStylusHandwritingEnabled();
+ }
+ }
}
diff --git a/core/haptics/haptics/build.gradle b/core/haptics/haptics/build.gradle
index 9fcb353..073a92e6 100644
--- a/core/haptics/haptics/build.gradle
+++ b/core/haptics/haptics/build.gradle
@@ -43,6 +43,7 @@
}
android {
+ compileSdk 35
namespace "androidx.core.haptics"
}
diff --git a/core/haptics/haptics/integration-tests/demos/build.gradle b/core/haptics/haptics/integration-tests/demos/build.gradle
index 9695d73..d4f87ae 100644
--- a/core/haptics/haptics/integration-tests/demos/build.gradle
+++ b/core/haptics/haptics/integration-tests/demos/build.gradle
@@ -28,6 +28,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.core.haptics.demos"
}
diff --git a/core/haptics/haptics/samples/build.gradle b/core/haptics/haptics/samples/build.gradle
index 2f6769e..3892ef1 100644
--- a/core/haptics/haptics/samples/build.gradle
+++ b/core/haptics/haptics/samples/build.gradle
@@ -25,6 +25,7 @@
implementation(project(":core:haptics:haptics"))
}
android {
+ compileSdk 35
namespace "androidx.core.haptics.samples"
}
androidx {
diff --git a/credentials/credentials-fido/build.gradle b/credentials/credentials-fido/build.gradle
index 8d0fd93..89b1cd52e 100644
--- a/credentials/credentials-fido/build.gradle
+++ b/credentials/credentials-fido/build.gradle
@@ -46,6 +46,7 @@
}
android {
+ compileSdk 35
namespace "androidx.credentials.fido"
}
diff --git a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt
+++ /dev/null
diff --git a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 6b0091c..72eaf1a 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -63,6 +63,7 @@
}
android {
+ compileSdk 35
namespace "androidx.credentials.play.services.auth"
buildTypes.configureEach {
diff --git a/credentials/credentials/api/1.3.0-beta01.txt b/credentials/credentials/api/1.3.0-beta01.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
- public final class ClearCredentialStateRequest {
- ctor public ClearCredentialStateRequest();
- }
-
- public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
- method public final String? getOrigin();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- method public final boolean preferImmediatelyAvailableCredentials();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String? origin;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String type;
- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- }
-
- public static final class CreateCredentialRequest.DisplayInfo {
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- method public CharSequence? getUserDisplayName();
- method public CharSequence getUserId();
- property public final CharSequence? userDisplayName;
- property public final CharSequence userId;
- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- }
-
- public abstract class CreateCredentialResponse {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
- }
-
- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePasswordRequest(String id, String password);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- }
-
- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePasswordResponse();
- }
-
- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
- method public String getRegistrationResponseJson();
- property public final String registrationResponseJson;
- }
-
- public abstract class Credential {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public interface CredentialManager {
- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public static androidx.credentials.CredentialManager create(android.content.Context context);
- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- field public static final androidx.credentials.CredentialManager.Companion Companion;
- }
-
- public static final class CredentialManager.Companion {
- method public androidx.credentials.CredentialManager create(android.content.Context context);
- }
-
- public interface CredentialManagerCallback<R, E> {
- method public void onError(E e);
- method public void onResult(R result);
- }
-
- public abstract class CredentialOption {
- method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final int getTypePriorityHint();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final java.util.Set<android.content.ComponentName> allowedProviders;
- property public final android.os.Bundle candidateQueryData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final android.os.Bundle requestData;
- property public final String type;
- property public final int typePriorityHint;
- field public static final androidx.credentials.CredentialOption.Companion Companion;
- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
- }
-
- public static final class CredentialOption.Companion {
- }
-
- public interface CredentialProvider {
- method public boolean isAvailableOnDevice();
- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- }
-
- public class CustomCredential extends androidx.credentials.Credential {
- ctor public CustomCredential(String type, android.os.Bundle data);
- }
-
- public final class GetCredentialRequest {
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- method public String? getOrigin();
- method public boolean getPreferIdentityDocUi();
- method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method public boolean preferImmediatelyAvailableCredentials();
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- property public final String? origin;
- property public final boolean preferIdentityDocUi;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final android.content.ComponentName? preferUiBrandingComponentName;
- }
-
- public static final class GetCredentialRequest.Builder {
- ctor public GetCredentialRequest.Builder();
- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
- method public androidx.credentials.GetCredentialRequest build();
- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
- }
-
- public final class GetCredentialResponse {
- ctor public GetCredentialResponse(androidx.credentials.Credential credential);
- method public androidx.credentials.Credential getCredential();
- property public final androidx.credentials.Credential credential;
- }
-
- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
- }
-
- public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class PasswordCredential extends androidx.credentials.Credential {
- ctor public PasswordCredential(String id, String password);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- field public static final androidx.credentials.PasswordCredential.Companion Companion;
- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
- }
-
- public static final class PasswordCredential.Companion {
- }
-
- @RequiresApi(34) public final class PrepareGetCredentialResponse {
- method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
- method public boolean isNullHandlesForTest();
- property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
- property public final boolean isNullHandlesForTest;
- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
- }
-
- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
- }
-
- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
- ctor public PrepareGetCredentialResponse.TestBuilder();
- method public androidx.credentials.PrepareGetCredentialResponse build();
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- }
-
- public final class PublicKeyCredential extends androidx.credentials.Credential {
- ctor public PublicKeyCredential(String authenticationResponseJson);
- method public String getAuthenticationResponseJson();
- property public final String authenticationResponseJson;
- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
- }
-
- public static final class PublicKeyCredential.Companion {
- }
-
-}
-
-package androidx.credentials.exceptions {
-
- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialCustomException(String type);
- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class ClearCredentialException extends java.lang.Exception {
- }
-
- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialInterruptedException();
- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialProviderConfigurationException();
- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnknownException();
- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnsupportedException();
- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCancellationException();
- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCustomException(String type);
- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class CreateCredentialException extends java.lang.Exception {
- }
-
- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialInterruptedException();
- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialNoCreateOptionException();
- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialProviderConfigurationException();
- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnknownException();
- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnsupportedException();
- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCancellationException();
- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCustomException(String type);
- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class GetCredentialException extends java.lang.Exception {
- }
-
- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialInterruptedException();
- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialProviderConfigurationException();
- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnknownException();
- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnsupportedException();
- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public NoCredentialException();
- ctor public NoCredentialException(optional CharSequence? errorMessage);
- }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public AbortError();
- }
-
- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ConstraintError();
- }
-
- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataCloneError();
- }
-
- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataError();
- }
-
- public abstract class DomError {
- ctor public DomError(String type);
- }
-
- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public EncodingError();
- }
-
- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public HierarchyRequestError();
- }
-
- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InUseAttributeError();
- }
-
- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidCharacterError();
- }
-
- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidModificationError();
- }
-
- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidNodeTypeError();
- }
-
- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidStateError();
- }
-
- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NamespaceError();
- }
-
- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NetworkError();
- }
-
- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NoModificationAllowedError();
- }
-
- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotAllowedError();
- }
-
- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotFoundError();
- }
-
- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotReadableError();
- }
-
- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotSupportedError();
- }
-
- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OperationError();
- }
-
- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OptOutError();
- }
-
- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public QuotaExceededError();
- }
-
- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ReadOnlyError();
- }
-
- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SecurityError();
- }
-
- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SyntaxError();
- }
-
- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TimeoutError();
- }
-
- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TransactionInactiveError();
- }
-
- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public UnknownError();
- }
-
- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public VersionError();
- }
-
- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public WrongDocumentError();
- }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
- }
-
- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- }
-
-}
-
-package androidx.credentials.provider {
-
- public final class Action {
- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.Action.Companion Companion;
- }
-
- public static final class Action.Builder {
- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.Action build();
- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
- }
-
- public static final class Action.Companion {
- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- }
-
- public final class AuthenticationAction {
- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
- }
-
- public static final class AuthenticationAction.Builder {
- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.AuthenticationAction build();
- }
-
- public static final class AuthenticationAction.Companion {
- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- }
-
- public abstract class BeginCreateCredentialRequest {
- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getType();
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- property public final android.os.Bundle candidateQueryData;
- property public final String type;
- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
- }
-
- public static final class BeginCreateCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginCreateCredentialResponse {
- ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
- }
-
- public static final class BeginCreateCredentialResponse.Builder {
- ctor public BeginCreateCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
- method public androidx.credentials.provider.BeginCreateCredentialResponse build();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginCreateCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- }
-
- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public abstract class BeginGetCredentialOption {
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getId();
- method public final String getType();
- property public final android.os.Bundle candidateQueryData;
- property public final String id;
- property public final String type;
- }
-
- public final class BeginGetCredentialRequest {
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
- }
-
- public static final class BeginGetCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginGetCredentialResponse {
- ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.Action> getActions();
- method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
- method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.Action> actions;
- property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
- property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
- }
-
- public static final class BeginGetCredentialResponse.Builder {
- ctor public BeginGetCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
- method public androidx.credentials.provider.BeginGetCredentialResponse build();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginGetCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CallingAppInfo {
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
- method public String? getOrigin(String privilegedAllowlist);
- method public String getPackageName();
- method public android.content.pm.SigningInfo getSigningInfo();
- method public boolean isOriginPopulated();
- property public final String packageName;
- property public final android.content.pm.SigningInfo signingInfo;
- }
-
- @RequiresApi(26) public final class CreateEntry {
- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- method public CharSequence getAccountName();
- method public CharSequence? getDescription();
- method public android.graphics.drawable.Icon? getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public Integer? getPasswordCredentialCount();
- method public android.app.PendingIntent getPendingIntent();
- method public Integer? getPublicKeyCredentialCount();
- method public Integer? getTotalCredentialCount();
- method public boolean isAutoSelectAllowed();
- property public final CharSequence accountName;
- property public final CharSequence? description;
- property public final android.graphics.drawable.Icon? icon;
- property public final boolean isAutoSelectAllowed;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
- }
-
- public static final class CreateEntry.Builder {
- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.CreateEntry build();
- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
- }
-
- public static final class CreateEntry.Companion {
- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- }
-
- public abstract class CredentialEntry {
- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public final CharSequence? getAffiliatedDomain();
- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
- method public final CharSequence getEntryGroupId();
- method public final boolean isDefaultIconPreferredAsSingleProvider();
- property public final CharSequence? affiliatedDomain;
- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
- property public final CharSequence entryGroupId;
- property public final boolean isDefaultIconPreferredAsSingleProvider;
- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
- }
-
- public static final class CredentialEntry.Companion {
- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
- ctor public CredentialProviderService();
- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- }
-
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public String getType();
- method public CharSequence? getTypeDisplayName();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- property public String type;
- property public final CharSequence? typeDisplayName;
- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
- }
-
- public static final class CustomCredentialEntry.Builder {
- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
- method public androidx.credentials.provider.CustomCredentialEntry build();
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
- }
-
- public static final class CustomCredentialEntry.Companion {
- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class IntentHandlerConverters {
- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
- }
-
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
- }
-
- public static final class PasswordCredentialEntry.Builder {
- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
- method public androidx.credentials.provider.PasswordCredentialEntry build();
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PasswordCredentialEntry.Companion {
- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public final class PendingIntentHandler {
- ctor public PendingIntentHandler();
- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
- }
-
- public static final class PendingIntentHandler.Companion {
- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- }
-
- public final class ProviderClearCredentialStateRequest {
- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- }
-
- public final class ProviderCreateCredentialRequest {
- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public androidx.credentials.CreateCredentialRequest getCallingRequest();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final androidx.credentials.CreateCredentialRequest callingRequest;
- }
-
- public final class ProviderGetCredentialRequest {
- ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- }
-
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
- }
-
- public static final class PublicKeyCredentialEntry.Builder {
- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
- method public androidx.credentials.provider.PublicKeyCredentialEntry build();
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PublicKeyCredentialEntry.Companion {
- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class RemoteEntry {
- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- method public android.app.PendingIntent getPendingIntent();
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
- }
-
- public static final class RemoteEntry.Builder {
- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.RemoteEntry build();
- }
-
- public static final class RemoteEntry.Companion {
- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- }
-
-}
-
diff --git a/credentials/credentials/api/1.3.0-beta02.txt b/credentials/credentials/api/1.3.0-beta02.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/1.3.0-beta02.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
- public final class ClearCredentialStateRequest {
- ctor public ClearCredentialStateRequest();
- }
-
- public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
- method public final String? getOrigin();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- method public final boolean preferImmediatelyAvailableCredentials();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String? origin;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String type;
- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- }
-
- public static final class CreateCredentialRequest.DisplayInfo {
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- method public CharSequence? getUserDisplayName();
- method public CharSequence getUserId();
- property public final CharSequence? userDisplayName;
- property public final CharSequence userId;
- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- }
-
- public abstract class CreateCredentialResponse {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
- }
-
- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePasswordRequest(String id, String password);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- }
-
- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePasswordResponse();
- }
-
- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
- method public String getRegistrationResponseJson();
- property public final String registrationResponseJson;
- }
-
- public abstract class Credential {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public interface CredentialManager {
- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public static androidx.credentials.CredentialManager create(android.content.Context context);
- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- field public static final androidx.credentials.CredentialManager.Companion Companion;
- }
-
- public static final class CredentialManager.Companion {
- method public androidx.credentials.CredentialManager create(android.content.Context context);
- }
-
- public interface CredentialManagerCallback<R, E> {
- method public void onError(E e);
- method public void onResult(R result);
- }
-
- public abstract class CredentialOption {
- method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final int getTypePriorityHint();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final java.util.Set<android.content.ComponentName> allowedProviders;
- property public final android.os.Bundle candidateQueryData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final android.os.Bundle requestData;
- property public final String type;
- property public final int typePriorityHint;
- field public static final androidx.credentials.CredentialOption.Companion Companion;
- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
- }
-
- public static final class CredentialOption.Companion {
- }
-
- public interface CredentialProvider {
- method public boolean isAvailableOnDevice();
- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- }
-
- public class CustomCredential extends androidx.credentials.Credential {
- ctor public CustomCredential(String type, android.os.Bundle data);
- }
-
- public final class GetCredentialRequest {
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- method public String? getOrigin();
- method public boolean getPreferIdentityDocUi();
- method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method public boolean preferImmediatelyAvailableCredentials();
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- property public final String? origin;
- property public final boolean preferIdentityDocUi;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final android.content.ComponentName? preferUiBrandingComponentName;
- }
-
- public static final class GetCredentialRequest.Builder {
- ctor public GetCredentialRequest.Builder();
- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
- method public androidx.credentials.GetCredentialRequest build();
- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
- }
-
- public final class GetCredentialResponse {
- ctor public GetCredentialResponse(androidx.credentials.Credential credential);
- method public androidx.credentials.Credential getCredential();
- property public final androidx.credentials.Credential credential;
- }
-
- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
- }
-
- public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class PasswordCredential extends androidx.credentials.Credential {
- ctor public PasswordCredential(String id, String password);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- field public static final androidx.credentials.PasswordCredential.Companion Companion;
- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
- }
-
- public static final class PasswordCredential.Companion {
- }
-
- @RequiresApi(34) public final class PrepareGetCredentialResponse {
- method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
- method public boolean isNullHandlesForTest();
- property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
- property public final boolean isNullHandlesForTest;
- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
- }
-
- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
- }
-
- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
- ctor public PrepareGetCredentialResponse.TestBuilder();
- method public androidx.credentials.PrepareGetCredentialResponse build();
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- }
-
- public final class PublicKeyCredential extends androidx.credentials.Credential {
- ctor public PublicKeyCredential(String authenticationResponseJson);
- method public String getAuthenticationResponseJson();
- property public final String authenticationResponseJson;
- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
- }
-
- public static final class PublicKeyCredential.Companion {
- }
-
-}
-
-package androidx.credentials.exceptions {
-
- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialCustomException(String type);
- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class ClearCredentialException extends java.lang.Exception {
- }
-
- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialInterruptedException();
- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialProviderConfigurationException();
- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnknownException();
- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnsupportedException();
- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCancellationException();
- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCustomException(String type);
- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class CreateCredentialException extends java.lang.Exception {
- }
-
- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialInterruptedException();
- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialNoCreateOptionException();
- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialProviderConfigurationException();
- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnknownException();
- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnsupportedException();
- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCancellationException();
- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCustomException(String type);
- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class GetCredentialException extends java.lang.Exception {
- }
-
- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialInterruptedException();
- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialProviderConfigurationException();
- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnknownException();
- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnsupportedException();
- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public NoCredentialException();
- ctor public NoCredentialException(optional CharSequence? errorMessage);
- }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public AbortError();
- }
-
- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ConstraintError();
- }
-
- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataCloneError();
- }
-
- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataError();
- }
-
- public abstract class DomError {
- ctor public DomError(String type);
- }
-
- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public EncodingError();
- }
-
- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public HierarchyRequestError();
- }
-
- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InUseAttributeError();
- }
-
- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidCharacterError();
- }
-
- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidModificationError();
- }
-
- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidNodeTypeError();
- }
-
- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidStateError();
- }
-
- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NamespaceError();
- }
-
- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NetworkError();
- }
-
- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NoModificationAllowedError();
- }
-
- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotAllowedError();
- }
-
- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotFoundError();
- }
-
- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotReadableError();
- }
-
- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotSupportedError();
- }
-
- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OperationError();
- }
-
- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OptOutError();
- }
-
- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public QuotaExceededError();
- }
-
- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ReadOnlyError();
- }
-
- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SecurityError();
- }
-
- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SyntaxError();
- }
-
- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TimeoutError();
- }
-
- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TransactionInactiveError();
- }
-
- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public UnknownError();
- }
-
- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public VersionError();
- }
-
- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public WrongDocumentError();
- }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
- }
-
- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- }
-
-}
-
-package androidx.credentials.provider {
-
- public final class Action {
- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.Action.Companion Companion;
- }
-
- public static final class Action.Builder {
- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.Action build();
- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
- }
-
- public static final class Action.Companion {
- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- }
-
- public final class AuthenticationAction {
- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
- }
-
- public static final class AuthenticationAction.Builder {
- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.AuthenticationAction build();
- }
-
- public static final class AuthenticationAction.Companion {
- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- }
-
- public abstract class BeginCreateCredentialRequest {
- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getType();
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- property public final android.os.Bundle candidateQueryData;
- property public final String type;
- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
- }
-
- public static final class BeginCreateCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginCreateCredentialResponse {
- ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
- }
-
- public static final class BeginCreateCredentialResponse.Builder {
- ctor public BeginCreateCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
- method public androidx.credentials.provider.BeginCreateCredentialResponse build();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginCreateCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- }
-
- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public abstract class BeginGetCredentialOption {
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getId();
- method public final String getType();
- property public final android.os.Bundle candidateQueryData;
- property public final String id;
- property public final String type;
- }
-
- public final class BeginGetCredentialRequest {
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
- }
-
- public static final class BeginGetCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginGetCredentialResponse {
- ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.Action> getActions();
- method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
- method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.Action> actions;
- property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
- property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
- }
-
- public static final class BeginGetCredentialResponse.Builder {
- ctor public BeginGetCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
- method public androidx.credentials.provider.BeginGetCredentialResponse build();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginGetCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CallingAppInfo {
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
- method public String? getOrigin(String privilegedAllowlist);
- method public String getPackageName();
- method public android.content.pm.SigningInfo getSigningInfo();
- method public boolean isOriginPopulated();
- property public final String packageName;
- property public final android.content.pm.SigningInfo signingInfo;
- }
-
- @RequiresApi(26) public final class CreateEntry {
- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- method public CharSequence getAccountName();
- method public CharSequence? getDescription();
- method public android.graphics.drawable.Icon? getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public Integer? getPasswordCredentialCount();
- method public android.app.PendingIntent getPendingIntent();
- method public Integer? getPublicKeyCredentialCount();
- method public Integer? getTotalCredentialCount();
- method public boolean isAutoSelectAllowed();
- property public final CharSequence accountName;
- property public final CharSequence? description;
- property public final android.graphics.drawable.Icon? icon;
- property public final boolean isAutoSelectAllowed;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
- }
-
- public static final class CreateEntry.Builder {
- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.CreateEntry build();
- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
- }
-
- public static final class CreateEntry.Companion {
- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- }
-
- public abstract class CredentialEntry {
- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public final CharSequence? getAffiliatedDomain();
- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
- method public final CharSequence getEntryGroupId();
- method public final boolean isDefaultIconPreferredAsSingleProvider();
- property public final CharSequence? affiliatedDomain;
- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
- property public final CharSequence entryGroupId;
- property public final boolean isDefaultIconPreferredAsSingleProvider;
- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
- }
-
- public static final class CredentialEntry.Companion {
- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
- ctor public CredentialProviderService();
- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- }
-
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public String getType();
- method public CharSequence? getTypeDisplayName();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- property public String type;
- property public final CharSequence? typeDisplayName;
- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
- }
-
- public static final class CustomCredentialEntry.Builder {
- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
- method public androidx.credentials.provider.CustomCredentialEntry build();
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
- }
-
- public static final class CustomCredentialEntry.Companion {
- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class IntentHandlerConverters {
- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
- }
-
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
- }
-
- public static final class PasswordCredentialEntry.Builder {
- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
- method public androidx.credentials.provider.PasswordCredentialEntry build();
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PasswordCredentialEntry.Companion {
- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public final class PendingIntentHandler {
- ctor public PendingIntentHandler();
- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
- }
-
- public static final class PendingIntentHandler.Companion {
- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- }
-
- public final class ProviderClearCredentialStateRequest {
- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- }
-
- public final class ProviderCreateCredentialRequest {
- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public androidx.credentials.CreateCredentialRequest getCallingRequest();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final androidx.credentials.CreateCredentialRequest callingRequest;
- }
-
- public final class ProviderGetCredentialRequest {
- ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- }
-
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
- }
-
- public static final class PublicKeyCredentialEntry.Builder {
- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
- method public androidx.credentials.provider.PublicKeyCredentialEntry build();
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PublicKeyCredentialEntry.Companion {
- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class RemoteEntry {
- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- method public android.app.PendingIntent getPendingIntent();
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
- }
-
- public static final class RemoteEntry.Builder {
- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.RemoteEntry build();
- }
-
- public static final class RemoteEntry.Companion {
- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- }
-
-}
-
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index a0d0eda..8b50742 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -133,6 +133,11 @@
method public void onResult(R result);
}
+ public final class CredentialManagerViewHandler {
+ method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
+ method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
+ }
+
public abstract class CredentialOption {
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
@@ -245,6 +250,14 @@
public static final class PasswordCredential.Companion {
}
+ public final class PendingGetCredentialRequest {
+ ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1<? super androidx.credentials.GetCredentialResponse,kotlin.Unit> callback);
+ method public kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> getCallback();
+ method public androidx.credentials.GetCredentialRequest getRequest();
+ property public final kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> callback;
+ property public final androidx.credentials.GetCredentialRequest request;
+ }
+
@RequiresApi(34) public final class PrepareGetCredentialResponse {
method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
@@ -591,6 +604,21 @@
method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
}
+ public final class AuthenticationError {
+ ctor public AuthenticationError(int errorCode);
+ ctor public AuthenticationError(int errorCode, optional CharSequence? errorMsg);
+ method public int getErrorCode();
+ method public CharSequence? getErrorMsg();
+ property public final int errorCode;
+ property public final CharSequence? errorMsg;
+ }
+
+ public final class AuthenticationResult {
+ ctor public AuthenticationResult(int authenticationType);
+ method public int getAuthenticationType();
+ property public final int authenticationType;
+ }
+
public abstract class BeginCreateCredentialRequest {
ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
@@ -729,6 +757,33 @@
property public final String requestJson;
}
+ @RequiresApi(35) public final class BiometricPromptData {
+ ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
+ ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
+ method public int getAllowedAuthenticators();
+ method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+ property public final int allowedAuthenticators;
+ property public final androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject;
+ }
+
+ public static final class BiometricPromptData.Builder {
+ ctor public BiometricPromptData.Builder();
+ method public androidx.credentials.provider.BiometricPromptData build();
+ method public androidx.credentials.provider.BiometricPromptData.Builder setAllowedAuthenticators(int allowedAuthenticators);
+ method public androidx.credentials.provider.BiometricPromptData.Builder setCryptoObject(androidx.biometric.BiometricPrompt.CryptoObject cryptoObject);
+ }
+
+ public final class BiometricPromptResult {
+ ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationError authenticationError);
+ ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationResult authenticationResult);
+ method public androidx.credentials.provider.AuthenticationError? getAuthenticationError();
+ method public androidx.credentials.provider.AuthenticationResult? getAuthenticationResult();
+ method public boolean isSuccessful();
+ property public final androidx.credentials.provider.AuthenticationError? authenticationError;
+ property public final androidx.credentials.provider.AuthenticationResult? authenticationResult;
+ property public final boolean isSuccessful;
+ }
+
public final class CallingAppInfo {
ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
@@ -740,10 +795,12 @@
property public final android.content.pm.SigningInfo signingInfo;
}
- @RequiresApi(26) public final class CreateEntry {
+ @RequiresApi(23) public final class CreateEntry {
ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
method public CharSequence getAccountName();
+ method public androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
method public CharSequence? getDescription();
method public android.graphics.drawable.Icon? getIcon();
method public java.time.Instant? getLastUsedTime();
@@ -753,6 +810,7 @@
method public Integer? getTotalCredentialCount();
method public boolean isAutoSelectAllowed();
property public final CharSequence accountName;
+ property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
property public final CharSequence? description;
property public final android.graphics.drawable.Icon? icon;
property public final boolean isAutoSelectAllowed;
@@ -765,6 +823,7 @@
ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
method public androidx.credentials.provider.CreateEntry build();
method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CreateEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
@@ -781,10 +840,12 @@
method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public final CharSequence? getAffiliatedDomain();
method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+ method public final androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
method public final CharSequence getEntryGroupId();
method public final boolean isDefaultIconPreferredAsSingleProvider();
property public final CharSequence? affiliatedDomain;
property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+ property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
property public final CharSequence entryGroupId;
property public final boolean isDefaultIconPreferredAsSingleProvider;
field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
@@ -804,9 +865,10 @@
method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
}
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public android.graphics.drawable.Icon getIcon();
method public java.time.Instant? getLastUsedTime();
@@ -835,6 +897,7 @@
ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
method public androidx.credentials.provider.CustomCredentialEntry build();
method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CustomCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -855,9 +918,10 @@
method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
}
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public CharSequence? getDisplayName();
method public android.graphics.drawable.Icon getIcon();
@@ -885,6 +949,7 @@
method public androidx.credentials.provider.PasswordCredentialEntry build();
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PasswordCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -927,23 +992,30 @@
public final class ProviderCreateCredentialRequest {
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+ property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final androidx.credentials.CreateCredentialRequest callingRequest;
}
public final class ProviderGetCredentialRequest {
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
}
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public CharSequence? getDisplayName();
method public android.graphics.drawable.Icon getIcon();
@@ -970,6 +1042,7 @@
ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
method public androidx.credentials.provider.PublicKeyCredentialEntry build();
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
diff --git a/credentials/credentials/api/res-1.3.0-beta01.txt b/credentials/credentials/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/credentials/credentials/api/res-1.3.0-beta02.txt b/credentials/credentials/api/res-1.3.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials/api/res-1.3.0-beta02.txt
+++ /dev/null
diff --git a/credentials/credentials/api/restricted_1.3.0-beta01.txt b/credentials/credentials/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
- public final class ClearCredentialStateRequest {
- ctor public ClearCredentialStateRequest();
- }
-
- public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
- method public final String? getOrigin();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- method public final boolean preferImmediatelyAvailableCredentials();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String? origin;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String type;
- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- }
-
- public static final class CreateCredentialRequest.DisplayInfo {
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- method public CharSequence? getUserDisplayName();
- method public CharSequence getUserId();
- property public final CharSequence? userDisplayName;
- property public final CharSequence userId;
- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- }
-
- public abstract class CreateCredentialResponse {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
- }
-
- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePasswordRequest(String id, String password);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- }
-
- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePasswordResponse();
- }
-
- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
- method public String getRegistrationResponseJson();
- property public final String registrationResponseJson;
- }
-
- public abstract class Credential {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public interface CredentialManager {
- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public static androidx.credentials.CredentialManager create(android.content.Context context);
- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- field public static final androidx.credentials.CredentialManager.Companion Companion;
- }
-
- public static final class CredentialManager.Companion {
- method public androidx.credentials.CredentialManager create(android.content.Context context);
- }
-
- public interface CredentialManagerCallback<R, E> {
- method public void onError(E e);
- method public void onResult(R result);
- }
-
- public abstract class CredentialOption {
- method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final int getTypePriorityHint();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final java.util.Set<android.content.ComponentName> allowedProviders;
- property public final android.os.Bundle candidateQueryData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final android.os.Bundle requestData;
- property public final String type;
- property public final int typePriorityHint;
- field public static final androidx.credentials.CredentialOption.Companion Companion;
- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
- }
-
- public static final class CredentialOption.Companion {
- }
-
- public interface CredentialProvider {
- method public boolean isAvailableOnDevice();
- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- }
-
- public class CustomCredential extends androidx.credentials.Credential {
- ctor public CustomCredential(String type, android.os.Bundle data);
- }
-
- public final class GetCredentialRequest {
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- method public String? getOrigin();
- method public boolean getPreferIdentityDocUi();
- method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method public boolean preferImmediatelyAvailableCredentials();
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- property public final String? origin;
- property public final boolean preferIdentityDocUi;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final android.content.ComponentName? preferUiBrandingComponentName;
- }
-
- public static final class GetCredentialRequest.Builder {
- ctor public GetCredentialRequest.Builder();
- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
- method public androidx.credentials.GetCredentialRequest build();
- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
- }
-
- public final class GetCredentialResponse {
- ctor public GetCredentialResponse(androidx.credentials.Credential credential);
- method public androidx.credentials.Credential getCredential();
- property public final androidx.credentials.Credential credential;
- }
-
- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
- }
-
- public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class PasswordCredential extends androidx.credentials.Credential {
- ctor public PasswordCredential(String id, String password);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- field public static final androidx.credentials.PasswordCredential.Companion Companion;
- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
- }
-
- public static final class PasswordCredential.Companion {
- }
-
- @RequiresApi(34) public final class PrepareGetCredentialResponse {
- method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
- method public boolean isNullHandlesForTest();
- property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
- property public final boolean isNullHandlesForTest;
- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
- }
-
- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
- }
-
- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
- ctor public PrepareGetCredentialResponse.TestBuilder();
- method public androidx.credentials.PrepareGetCredentialResponse build();
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- }
-
- public final class PublicKeyCredential extends androidx.credentials.Credential {
- ctor public PublicKeyCredential(String authenticationResponseJson);
- method public String getAuthenticationResponseJson();
- property public final String authenticationResponseJson;
- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
- }
-
- public static final class PublicKeyCredential.Companion {
- }
-
-}
-
-package androidx.credentials.exceptions {
-
- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialCustomException(String type);
- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class ClearCredentialException extends java.lang.Exception {
- }
-
- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialInterruptedException();
- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialProviderConfigurationException();
- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnknownException();
- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnsupportedException();
- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCancellationException();
- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCustomException(String type);
- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class CreateCredentialException extends java.lang.Exception {
- }
-
- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialInterruptedException();
- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialNoCreateOptionException();
- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialProviderConfigurationException();
- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnknownException();
- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnsupportedException();
- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCancellationException();
- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCustomException(String type);
- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class GetCredentialException extends java.lang.Exception {
- }
-
- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialInterruptedException();
- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialProviderConfigurationException();
- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnknownException();
- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnsupportedException();
- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public NoCredentialException();
- ctor public NoCredentialException(optional CharSequence? errorMessage);
- }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public AbortError();
- }
-
- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ConstraintError();
- }
-
- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataCloneError();
- }
-
- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataError();
- }
-
- public abstract class DomError {
- ctor public DomError(String type);
- }
-
- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public EncodingError();
- }
-
- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public HierarchyRequestError();
- }
-
- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InUseAttributeError();
- }
-
- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidCharacterError();
- }
-
- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidModificationError();
- }
-
- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidNodeTypeError();
- }
-
- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidStateError();
- }
-
- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NamespaceError();
- }
-
- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NetworkError();
- }
-
- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NoModificationAllowedError();
- }
-
- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotAllowedError();
- }
-
- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotFoundError();
- }
-
- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotReadableError();
- }
-
- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotSupportedError();
- }
-
- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OperationError();
- }
-
- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OptOutError();
- }
-
- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public QuotaExceededError();
- }
-
- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ReadOnlyError();
- }
-
- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SecurityError();
- }
-
- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SyntaxError();
- }
-
- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TimeoutError();
- }
-
- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TransactionInactiveError();
- }
-
- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public UnknownError();
- }
-
- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public VersionError();
- }
-
- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public WrongDocumentError();
- }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
- }
-
- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- }
-
-}
-
-package androidx.credentials.provider {
-
- public final class Action {
- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.Action.Companion Companion;
- }
-
- public static final class Action.Builder {
- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.Action build();
- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
- }
-
- public static final class Action.Companion {
- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- }
-
- public final class AuthenticationAction {
- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
- }
-
- public static final class AuthenticationAction.Builder {
- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.AuthenticationAction build();
- }
-
- public static final class AuthenticationAction.Companion {
- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- }
-
- public abstract class BeginCreateCredentialRequest {
- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getType();
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- property public final android.os.Bundle candidateQueryData;
- property public final String type;
- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
- }
-
- public static final class BeginCreateCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginCreateCredentialResponse {
- ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
- }
-
- public static final class BeginCreateCredentialResponse.Builder {
- ctor public BeginCreateCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
- method public androidx.credentials.provider.BeginCreateCredentialResponse build();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginCreateCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- }
-
- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public abstract class BeginGetCredentialOption {
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getId();
- method public final String getType();
- property public final android.os.Bundle candidateQueryData;
- property public final String id;
- property public final String type;
- }
-
- public final class BeginGetCredentialRequest {
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
- }
-
- public static final class BeginGetCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginGetCredentialResponse {
- ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.Action> getActions();
- method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
- method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.Action> actions;
- property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
- property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
- }
-
- public static final class BeginGetCredentialResponse.Builder {
- ctor public BeginGetCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
- method public androidx.credentials.provider.BeginGetCredentialResponse build();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginGetCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CallingAppInfo {
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
- method public String? getOrigin(String privilegedAllowlist);
- method public String getPackageName();
- method public android.content.pm.SigningInfo getSigningInfo();
- method public boolean isOriginPopulated();
- property public final String packageName;
- property public final android.content.pm.SigningInfo signingInfo;
- }
-
- @RequiresApi(26) public final class CreateEntry {
- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- method public CharSequence getAccountName();
- method public CharSequence? getDescription();
- method public android.graphics.drawable.Icon? getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public Integer? getPasswordCredentialCount();
- method public android.app.PendingIntent getPendingIntent();
- method public Integer? getPublicKeyCredentialCount();
- method public Integer? getTotalCredentialCount();
- method public boolean isAutoSelectAllowed();
- property public final CharSequence accountName;
- property public final CharSequence? description;
- property public final android.graphics.drawable.Icon? icon;
- property public final boolean isAutoSelectAllowed;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
- }
-
- public static final class CreateEntry.Builder {
- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.CreateEntry build();
- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
- }
-
- public static final class CreateEntry.Companion {
- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- }
-
- public abstract class CredentialEntry {
- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public final CharSequence? getAffiliatedDomain();
- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
- method public final CharSequence getEntryGroupId();
- method public final boolean isDefaultIconPreferredAsSingleProvider();
- property public final CharSequence? affiliatedDomain;
- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
- property public final CharSequence entryGroupId;
- property public final boolean isDefaultIconPreferredAsSingleProvider;
- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
- }
-
- public static final class CredentialEntry.Companion {
- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
- ctor public CredentialProviderService();
- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- }
-
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public String getType();
- method public CharSequence? getTypeDisplayName();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- property public String type;
- property public final CharSequence? typeDisplayName;
- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
- }
-
- public static final class CustomCredentialEntry.Builder {
- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
- method public androidx.credentials.provider.CustomCredentialEntry build();
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
- }
-
- public static final class CustomCredentialEntry.Companion {
- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class IntentHandlerConverters {
- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
- }
-
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
- }
-
- public static final class PasswordCredentialEntry.Builder {
- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
- method public androidx.credentials.provider.PasswordCredentialEntry build();
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PasswordCredentialEntry.Companion {
- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public final class PendingIntentHandler {
- ctor public PendingIntentHandler();
- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
- }
-
- public static final class PendingIntentHandler.Companion {
- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- }
-
- public final class ProviderClearCredentialStateRequest {
- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- }
-
- public final class ProviderCreateCredentialRequest {
- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public androidx.credentials.CreateCredentialRequest getCallingRequest();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final androidx.credentials.CreateCredentialRequest callingRequest;
- }
-
- public final class ProviderGetCredentialRequest {
- ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- }
-
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
- }
-
- public static final class PublicKeyCredentialEntry.Builder {
- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
- method public androidx.credentials.provider.PublicKeyCredentialEntry build();
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PublicKeyCredentialEntry.Companion {
- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class RemoteEntry {
- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- method public android.app.PendingIntent getPendingIntent();
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
- }
-
- public static final class RemoteEntry.Builder {
- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.RemoteEntry build();
- }
-
- public static final class RemoteEntry.Companion {
- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- }
-
-}
-
diff --git a/credentials/credentials/api/restricted_1.3.0-beta02.txt b/credentials/credentials/api/restricted_1.3.0-beta02.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/restricted_1.3.0-beta02.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
- public final class ClearCredentialStateRequest {
- ctor public ClearCredentialStateRequest();
- }
-
- public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
- method public final String? getOrigin();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- method public final boolean preferImmediatelyAvailableCredentials();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String? origin;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String type;
- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
- }
-
- public static final class CreateCredentialRequest.DisplayInfo {
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- method public CharSequence? getUserDisplayName();
- method public CharSequence getUserId();
- property public final CharSequence? userDisplayName;
- property public final CharSequence userId;
- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
- }
-
- public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
- }
-
- public abstract class CreateCredentialResponse {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
- }
-
- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePasswordRequest(String id, String password);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- }
-
- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePasswordResponse();
- }
-
- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
- method public String getRegistrationResponseJson();
- property public final String registrationResponseJson;
- }
-
- public abstract class Credential {
- method public final android.os.Bundle getData();
- method public final String getType();
- property public final android.os.Bundle data;
- property public final String type;
- }
-
- public interface CredentialManager {
- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public static androidx.credentials.CredentialManager create(android.content.Context context);
- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- field public static final androidx.credentials.CredentialManager.Companion Companion;
- }
-
- public static final class CredentialManager.Companion {
- method public androidx.credentials.CredentialManager create(android.content.Context context);
- }
-
- public interface CredentialManagerCallback<R, E> {
- method public void onError(E e);
- method public void onResult(R result);
- }
-
- public abstract class CredentialOption {
- method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final int getTypePriorityHint();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final java.util.Set<android.content.ComponentName> allowedProviders;
- property public final android.os.Bundle candidateQueryData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final android.os.Bundle requestData;
- property public final String type;
- property public final int typePriorityHint;
- field public static final androidx.credentials.CredentialOption.Companion Companion;
- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
- }
-
- public static final class CredentialOption.Companion {
- }
-
- public interface CredentialProvider {
- method public boolean isAvailableOnDevice();
- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- }
-
- public class CustomCredential extends androidx.credentials.Credential {
- ctor public CustomCredential(String type, android.os.Bundle data);
- }
-
- public final class GetCredentialRequest {
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
- ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- method public String? getOrigin();
- method public boolean getPreferIdentityDocUi();
- method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method public boolean preferImmediatelyAvailableCredentials();
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- property public final String? origin;
- property public final boolean preferIdentityDocUi;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final android.content.ComponentName? preferUiBrandingComponentName;
- }
-
- public static final class GetCredentialRequest.Builder {
- ctor public GetCredentialRequest.Builder();
- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
- method public androidx.credentials.GetCredentialRequest build();
- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
- }
-
- public final class GetCredentialResponse {
- ctor public GetCredentialResponse(androidx.credentials.Credential credential);
- method public androidx.credentials.Credential getCredential();
- property public final androidx.credentials.Credential credential;
- }
-
- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
- }
-
- public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
- ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class PasswordCredential extends androidx.credentials.Credential {
- ctor public PasswordCredential(String id, String password);
- method public String getId();
- method public String getPassword();
- property public final String id;
- property public final String password;
- field public static final androidx.credentials.PasswordCredential.Companion Companion;
- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
- }
-
- public static final class PasswordCredential.Companion {
- }
-
- @RequiresApi(34) public final class PrepareGetCredentialResponse {
- method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
- method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
- method public boolean isNullHandlesForTest();
- property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
- property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
- property public final boolean isNullHandlesForTest;
- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
- }
-
- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
- }
-
- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
- ctor public PrepareGetCredentialResponse.TestBuilder();
- method public androidx.credentials.PrepareGetCredentialResponse build();
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
- }
-
- public final class PublicKeyCredential extends androidx.credentials.Credential {
- ctor public PublicKeyCredential(String authenticationResponseJson);
- method public String getAuthenticationResponseJson();
- property public final String authenticationResponseJson;
- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
- }
-
- public static final class PublicKeyCredential.Companion {
- }
-
-}
-
-package androidx.credentials.exceptions {
-
- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialCustomException(String type);
- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class ClearCredentialException extends java.lang.Exception {
- }
-
- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialInterruptedException();
- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialProviderConfigurationException();
- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnknownException();
- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
- ctor public ClearCredentialUnsupportedException();
- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCancellationException();
- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCustomException(String type);
- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class CreateCredentialException extends java.lang.Exception {
- }
-
- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialInterruptedException();
- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialNoCreateOptionException();
- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialProviderConfigurationException();
- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnknownException();
- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialUnsupportedException();
- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCancellationException();
- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCustomException(String type);
- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
- method public String getType();
- property public String type;
- }
-
- public abstract class GetCredentialException extends java.lang.Exception {
- }
-
- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialInterruptedException();
- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialProviderConfigurationException();
- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnknownException();
- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
- }
-
- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialUnsupportedException();
- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
- }
-
- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public NoCredentialException();
- ctor public NoCredentialException(optional CharSequence? errorMessage);
- }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public AbortError();
- }
-
- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ConstraintError();
- }
-
- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataCloneError();
- }
-
- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public DataError();
- }
-
- public abstract class DomError {
- ctor public DomError(String type);
- }
-
- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public EncodingError();
- }
-
- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public HierarchyRequestError();
- }
-
- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InUseAttributeError();
- }
-
- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidCharacterError();
- }
-
- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidModificationError();
- }
-
- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidNodeTypeError();
- }
-
- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public InvalidStateError();
- }
-
- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NamespaceError();
- }
-
- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NetworkError();
- }
-
- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NoModificationAllowedError();
- }
-
- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotAllowedError();
- }
-
- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotFoundError();
- }
-
- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotReadableError();
- }
-
- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public NotSupportedError();
- }
-
- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OperationError();
- }
-
- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public OptOutError();
- }
-
- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public QuotaExceededError();
- }
-
- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public ReadOnlyError();
- }
-
- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SecurityError();
- }
-
- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public SyntaxError();
- }
-
- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TimeoutError();
- }
-
- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public TransactionInactiveError();
- }
-
- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public UnknownError();
- }
-
- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public VersionError();
- }
-
- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
- ctor public WrongDocumentError();
- }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
- }
-
- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
- method public androidx.credentials.exceptions.domerrors.DomError getDomError();
- property public final androidx.credentials.exceptions.domerrors.DomError domError;
- }
-
- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
- }
-
-}
-
-package androidx.credentials.provider {
-
- public final class Action {
- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.Action.Companion Companion;
- }
-
- public static final class Action.Builder {
- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.Action build();
- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
- }
-
- public static final class Action.Companion {
- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
- }
-
- public final class AuthenticationAction {
- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTitle();
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence title;
- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
- }
-
- public static final class AuthenticationAction.Builder {
- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.AuthenticationAction build();
- }
-
- public static final class AuthenticationAction.Companion {
- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
- }
-
- public abstract class BeginCreateCredentialRequest {
- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getType();
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- property public final android.os.Bundle candidateQueryData;
- property public final String type;
- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
- }
-
- public static final class BeginCreateCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginCreateCredentialResponse {
- ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
- }
-
- public static final class BeginCreateCredentialResponse.Builder {
- ctor public BeginCreateCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
- method public androidx.credentials.provider.BeginCreateCredentialResponse build();
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginCreateCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- }
-
- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public abstract class BeginGetCredentialOption {
- method public final android.os.Bundle getCandidateQueryData();
- method public final String getId();
- method public final String getType();
- property public final android.os.Bundle candidateQueryData;
- property public final String id;
- property public final String type;
- }
-
- public final class BeginGetCredentialRequest {
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
- ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
- property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
- }
-
- public static final class BeginGetCredentialRequest.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
- }
-
- public final class BeginGetCredentialResponse {
- ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- method public java.util.List<androidx.credentials.provider.Action> getActions();
- method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
- method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
- property public final java.util.List<androidx.credentials.provider.Action> actions;
- property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
- property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
- }
-
- public static final class BeginGetCredentialResponse.Builder {
- ctor public BeginGetCredentialResponse.Builder();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
- method public androidx.credentials.provider.BeginGetCredentialResponse build();
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
- }
-
- public static final class BeginGetCredentialResponse.Companion {
- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
- }
-
- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
- }
-
- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
- method public java.util.Set<java.lang.String> getAllowedUserIds();
- property public final java.util.Set<java.lang.String> allowedUserIds;
- }
-
- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
- method public byte[]? getClientDataHash();
- method public String getRequestJson();
- property public final byte[]? clientDataHash;
- property public final String requestJson;
- }
-
- public final class CallingAppInfo {
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
- method public String? getOrigin(String privilegedAllowlist);
- method public String getPackageName();
- method public android.content.pm.SigningInfo getSigningInfo();
- method public boolean isOriginPopulated();
- property public final String packageName;
- property public final android.content.pm.SigningInfo signingInfo;
- }
-
- @RequiresApi(26) public final class CreateEntry {
- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- method public CharSequence getAccountName();
- method public CharSequence? getDescription();
- method public android.graphics.drawable.Icon? getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public Integer? getPasswordCredentialCount();
- method public android.app.PendingIntent getPendingIntent();
- method public Integer? getPublicKeyCredentialCount();
- method public Integer? getTotalCredentialCount();
- method public boolean isAutoSelectAllowed();
- property public final CharSequence accountName;
- property public final CharSequence? description;
- property public final android.graphics.drawable.Icon? icon;
- property public final boolean isAutoSelectAllowed;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
- }
-
- public static final class CreateEntry.Builder {
- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.CreateEntry build();
- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
- }
-
- public static final class CreateEntry.Companion {
- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
- }
-
- public abstract class CredentialEntry {
- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public final CharSequence? getAffiliatedDomain();
- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
- method public final CharSequence getEntryGroupId();
- method public final boolean isDefaultIconPreferredAsSingleProvider();
- property public final CharSequence? affiliatedDomain;
- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
- property public final CharSequence entryGroupId;
- property public final boolean isDefaultIconPreferredAsSingleProvider;
- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
- }
-
- public static final class CredentialEntry.Companion {
- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
- ctor public CredentialProviderService();
- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
- }
-
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public String getType();
- method public CharSequence? getTypeDisplayName();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence? subtitle;
- property public final CharSequence title;
- property public String type;
- property public final CharSequence? typeDisplayName;
- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
- }
-
- public static final class CustomCredentialEntry.Builder {
- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
- method public androidx.credentials.provider.CustomCredentialEntry build();
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
- }
-
- public static final class CustomCredentialEntry.Companion {
- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class IntentHandlerConverters {
- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
- }
-
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
- }
-
- public static final class PasswordCredentialEntry.Builder {
- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
- method public androidx.credentials.provider.PasswordCredentialEntry build();
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PasswordCredentialEntry.Companion {
- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- @RequiresApi(34) public final class PendingIntentHandler {
- ctor public PendingIntentHandler();
- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
- }
-
- public static final class PendingIntentHandler.Companion {
- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
- }
-
- public final class ProviderClearCredentialStateRequest {
- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- }
-
- public final class ProviderCreateCredentialRequest {
- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public androidx.credentials.CreateCredentialRequest getCallingRequest();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final androidx.credentials.CreateCredentialRequest callingRequest;
- }
-
- public final class ProviderGetCredentialRequest {
- ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
- method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
- property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
- }
-
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- method public CharSequence? getDisplayName();
- method public android.graphics.drawable.Icon getIcon();
- method public java.time.Instant? getLastUsedTime();
- method public android.app.PendingIntent getPendingIntent();
- method public CharSequence getTypeDisplayName();
- method public CharSequence getUsername();
- method public boolean hasDefaultIcon();
- method public boolean isAutoSelectAllowed();
- method public boolean isAutoSelectAllowedFromOption();
- property public final CharSequence? displayName;
- property public final boolean hasDefaultIcon;
- property public final android.graphics.drawable.Icon icon;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isAutoSelectAllowedFromOption;
- property public final java.time.Instant? lastUsedTime;
- property public final android.app.PendingIntent pendingIntent;
- property public final CharSequence typeDisplayName;
- property public final CharSequence username;
- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
- }
-
- public static final class PublicKeyCredentialEntry.Builder {
- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
- method public androidx.credentials.provider.PublicKeyCredentialEntry build();
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
- }
-
- public static final class PublicKeyCredentialEntry.Companion {
- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
- }
-
- public final class RemoteEntry {
- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- method public android.app.PendingIntent getPendingIntent();
- property public final android.app.PendingIntent pendingIntent;
- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
- }
-
- public static final class RemoteEntry.Builder {
- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
- method public androidx.credentials.provider.RemoteEntry build();
- }
-
- public static final class RemoteEntry.Companion {
- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
- }
-
-}
-
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index a0d0eda..8b50742 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -133,6 +133,11 @@
method public void onResult(R result);
}
+ public final class CredentialManagerViewHandler {
+ method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
+ method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
+ }
+
public abstract class CredentialOption {
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
@@ -245,6 +250,14 @@
public static final class PasswordCredential.Companion {
}
+ public final class PendingGetCredentialRequest {
+ ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1<? super androidx.credentials.GetCredentialResponse,kotlin.Unit> callback);
+ method public kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> getCallback();
+ method public androidx.credentials.GetCredentialRequest getRequest();
+ property public final kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> callback;
+ property public final androidx.credentials.GetCredentialRequest request;
+ }
+
@RequiresApi(34) public final class PrepareGetCredentialResponse {
method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
@@ -591,6 +604,21 @@
method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
}
+ public final class AuthenticationError {
+ ctor public AuthenticationError(int errorCode);
+ ctor public AuthenticationError(int errorCode, optional CharSequence? errorMsg);
+ method public int getErrorCode();
+ method public CharSequence? getErrorMsg();
+ property public final int errorCode;
+ property public final CharSequence? errorMsg;
+ }
+
+ public final class AuthenticationResult {
+ ctor public AuthenticationResult(int authenticationType);
+ method public int getAuthenticationType();
+ property public final int authenticationType;
+ }
+
public abstract class BeginCreateCredentialRequest {
ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
@@ -729,6 +757,33 @@
property public final String requestJson;
}
+ @RequiresApi(35) public final class BiometricPromptData {
+ ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
+ ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
+ method public int getAllowedAuthenticators();
+ method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+ property public final int allowedAuthenticators;
+ property public final androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject;
+ }
+
+ public static final class BiometricPromptData.Builder {
+ ctor public BiometricPromptData.Builder();
+ method public androidx.credentials.provider.BiometricPromptData build();
+ method public androidx.credentials.provider.BiometricPromptData.Builder setAllowedAuthenticators(int allowedAuthenticators);
+ method public androidx.credentials.provider.BiometricPromptData.Builder setCryptoObject(androidx.biometric.BiometricPrompt.CryptoObject cryptoObject);
+ }
+
+ public final class BiometricPromptResult {
+ ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationError authenticationError);
+ ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationResult authenticationResult);
+ method public androidx.credentials.provider.AuthenticationError? getAuthenticationError();
+ method public androidx.credentials.provider.AuthenticationResult? getAuthenticationResult();
+ method public boolean isSuccessful();
+ property public final androidx.credentials.provider.AuthenticationError? authenticationError;
+ property public final androidx.credentials.provider.AuthenticationResult? authenticationResult;
+ property public final boolean isSuccessful;
+ }
+
public final class CallingAppInfo {
ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
@@ -740,10 +795,12 @@
property public final android.content.pm.SigningInfo signingInfo;
}
- @RequiresApi(26) public final class CreateEntry {
+ @RequiresApi(23) public final class CreateEntry {
ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
method public CharSequence getAccountName();
+ method public androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
method public CharSequence? getDescription();
method public android.graphics.drawable.Icon? getIcon();
method public java.time.Instant? getLastUsedTime();
@@ -753,6 +810,7 @@
method public Integer? getTotalCredentialCount();
method public boolean isAutoSelectAllowed();
property public final CharSequence accountName;
+ property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
property public final CharSequence? description;
property public final android.graphics.drawable.Icon? icon;
property public final boolean isAutoSelectAllowed;
@@ -765,6 +823,7 @@
ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
method public androidx.credentials.provider.CreateEntry build();
method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CreateEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
@@ -781,10 +840,12 @@
method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public final CharSequence? getAffiliatedDomain();
method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+ method public final androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
method public final CharSequence getEntryGroupId();
method public final boolean isDefaultIconPreferredAsSingleProvider();
property public final CharSequence? affiliatedDomain;
property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+ property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
property public final CharSequence entryGroupId;
property public final boolean isDefaultIconPreferredAsSingleProvider;
field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
@@ -804,9 +865,10 @@
method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
}
- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public android.graphics.drawable.Icon getIcon();
method public java.time.Instant? getLastUsedTime();
@@ -835,6 +897,7 @@
ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
method public androidx.credentials.provider.CustomCredentialEntry build();
method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CustomCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -855,9 +918,10 @@
method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
}
- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public CharSequence? getDisplayName();
method public android.graphics.drawable.Icon getIcon();
@@ -885,6 +949,7 @@
method public androidx.credentials.provider.PasswordCredentialEntry build();
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PasswordCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -927,23 +992,30 @@
public final class ProviderCreateCredentialRequest {
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+ property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final androidx.credentials.CreateCredentialRequest callingRequest;
}
public final class ProviderGetCredentialRequest {
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
}
- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
method public CharSequence? getDisplayName();
method public android.graphics.drawable.Icon getIcon();
@@ -970,6 +1042,7 @@
ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
method public androidx.credentials.provider.PublicKeyCredentialEntry build();
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index da177eb..37791f3 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -31,8 +31,10 @@
dependencies {
api("androidx.annotation:annotation:1.5.0")
+ api("androidx.biometric:biometric-ktx:1.4.0-alpha01")
api(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
+ implementation("androidx.core:core:1.15.0-alpha01")
androidTestImplementation("androidx.activity:activity:1.2.0")
androidTestImplementation(libs.junit)
@@ -41,6 +43,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":core:core"))
androidTestImplementation(project(":internal-testutils-truth"))
androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation(project(":internal-testutils-runtime"), {
@@ -49,6 +52,7 @@
}
android {
+ compileSdk = 35
namespace "androidx.credentials"
defaultConfig {
diff --git a/credentials/credentials/samples/build.gradle b/credentials/credentials/samples/build.gradle
index b0b0178..fa9238c 100644
--- a/credentials/credentials/samples/build.gradle
+++ b/credentials/credentials/samples/build.gradle
@@ -30,6 +30,7 @@
}
android {
+ compileSdk 35
namespace "androidx.credentials.samples"
defaultConfig {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
index 81f3196..390b9db 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
@@ -116,7 +116,7 @@
assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(expectedDefaultProvider);
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
public void constructFromBundle_success() {
String expectedUserId = "userId";
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
index ab9bb18..a4a07e29 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
@@ -101,7 +101,7 @@
assertThat(displayInfo.preferDefaultProvider).isEqualTo(expectedDefaultProvider)
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
fun constructFromBundle_success() {
val expectedUserId = "userId"
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
index 1e8d7ad..a8579df 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -139,7 +139,7 @@
assertThat(request.getPassword()).isEqualTo(passwordExpected);
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@SuppressWarnings("deprecation") // bundle.get(key)
@Test
public void getter_frameworkProperties() {
@@ -191,7 +191,7 @@
).isEqualTo(R.drawable.ic_password);
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
public void frameworkConversion_success() {
String idExpected = "id";
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 3f9e8dd..db25fc5 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -126,7 +126,7 @@
assertThat(request.password).isEqualTo(passwordExpected)
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Suppress("DEPRECATION") // bundle.get(key)
@Test
fun getter_frameworkProperties() {
@@ -189,7 +189,7 @@
.isEqualTo(R.drawable.ic_password)
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
fun frameworkConversion_success() {
val idExpected = "id"
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index cb095bb..893d0f9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -154,7 +154,7 @@
assertThat(testJsonActual).isEqualTo(testJsonExpected);
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@SuppressWarnings("deprecation") // bundle.get(key)
@Test
public void getter_frameworkProperties_success() {
@@ -210,7 +210,7 @@
).isEqualTo(R.drawable.ic_passkey);
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
public void frameworkConversion_success() {
byte[] clientDataHashExpected = "hash".getBytes();
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index 4715c85..cd3b53c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -140,7 +140,7 @@
assertThat(testJsonActual).isEqualTo(testJsonExpected)
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Suppress("DEPRECATION") // bundle.get(key)
@Test
fun getter_frameworkProperties_success() {
@@ -211,7 +211,7 @@
.isEqualTo(R.drawable.ic_passkey)
}
- @SdkSuppress(minSdkVersion = 28)
+ @SdkSuppress(minSdkVersion = 34)
@Test
fun frameworkConversion_success() {
val clientDataHashExpected = "hash".toByteArray()
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java
new file mode 100644
index 0000000..06f71a5
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.credentials.Credential;
+import android.os.OutcomeReceiver;
+import android.widget.EditText;
+
+import androidx.annotation.RequiresApi;
+import androidx.credentials.internal.FrameworkImplHelper;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import kotlin.Unit;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+public class CredentialManagerViewHandlerJavaTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ private static final GetCredentialRequest GET_CRED_PASSWORD_REQ =
+ new GetCredentialRequest.Builder()
+ .setCredentialOptions(Collections.singletonList(
+ new GetPasswordOption())).build();
+ private static final android.credentials.GetCredentialRequest GET_CRED_PASSWORD_FRAMEWORK_REQ =
+ FrameworkImplHelper.convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ);
+
+ @Test
+ @RequiresApi(35)
+ public void setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
+ EditText editText = new EditText(mContext);
+
+ PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
+ GET_CRED_PASSWORD_REQ,
+ (response) -> Unit.INSTANCE);
+
+ CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
+ pendingGetCredentialRequest);
+
+ assertNotNull(editText.getPendingCredentialRequest());
+ TestUtilsKt.equals(editText.getPendingCredentialRequest(),
+ GET_CRED_PASSWORD_FRAMEWORK_REQ);
+ assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
+ OutcomeReceiver.class
+ );
+ }
+
+ @Test
+ @RequiresApi(35)
+ public void setPendingCredentialRequest_callbackInvokedSuccessfully()
+ throws InterruptedException {
+ CountDownLatch latch1 = new CountDownLatch(1);
+ AtomicReference<GetCredentialResponse> getCredentialResponse = new AtomicReference<>();
+ EditText editText = new EditText(mContext);
+
+ PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
+ GET_CRED_PASSWORD_REQ,
+ (response) -> {
+ getCredentialResponse.set(response);
+ latch1.countDown();
+ return Unit.INSTANCE;
+ });
+
+ CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
+ pendingGetCredentialRequest);
+
+ assertNotNull(editText.getPendingCredentialRequest());
+ TestUtilsKt.equals(editText.getPendingCredentialRequest(), GET_CRED_PASSWORD_FRAMEWORK_REQ);
+ assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
+ OutcomeReceiver.class
+ );
+
+ PasswordCredential passwordCredential = new PasswordCredential("id", "password");
+ android.credentials.GetCredentialResponse frameworkPasswordResponse =
+ new android.credentials.GetCredentialResponse(new Credential(
+ passwordCredential.getType(), passwordCredential.getData()));
+ assertNotNull(editText.getPendingCredentialCallback());
+ editText.getPendingCredentialCallback().onResult(frameworkPasswordResponse);
+ latch1.await(50L, TimeUnit.MILLISECONDS);
+
+ assertThat(getCredentialResponse.get()).isNotNull();
+ GetCredentialResponse expectedGetCredentialResponse = FrameworkImplHelper
+ .convertGetResponseToJetpackClass(frameworkPasswordResponse);
+ TestUtilsKt.equals(expectedGetCredentialResponse, getCredentialResponse.get());
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
new file mode 100644
index 0000000..d0e4b6b
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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.credentials
+
+import android.content.Context
+import android.credentials.Credential
+import android.os.OutcomeReceiver
+import android.widget.EditText
+import androidx.annotation.RequiresApi
+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetRequestToFrameworkClass
+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetResponseToJetpackClass
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+class CredentialManagerViewHandlerTest {
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+ companion object {
+ private val GET_CRED_PASSWORD_REQ =
+ GetCredentialRequest.Builder().setCredentialOptions(listOf(GetPasswordOption())).build()
+ private val GET_CRED_PASSWORD_FRAMEWORK_REQ =
+ convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ)
+ }
+
+ @Test
+ @RequiresApi(35)
+ fun setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
+ val editText = EditText(mContext)
+ val pendingGetCredentialRequest =
+ PendingGetCredentialRequest(GET_CRED_PASSWORD_REQ) { _: GetCredentialResponse? -> }
+
+ editText.pendingGetCredentialRequest = pendingGetCredentialRequest
+
+ equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)
+ assertThat(editText.pendingCredentialCallback).isInstanceOf(OutcomeReceiver::class.java)
+ }
+
+ @Test
+ @RequiresApi(35)
+ @Throws(InterruptedException::class)
+ fun setPendingCredentialRequest_callbackInvokedSuccessfully() {
+ val latch1 = CountDownLatch(1)
+ val getCredentialResponse = AtomicReference<GetCredentialResponse>()
+ val editText = EditText(mContext)
+
+ editText.pendingGetCredentialRequest =
+ PendingGetCredentialRequest(GET_CRED_PASSWORD_REQ) { response ->
+ getCredentialResponse.set(response)
+ latch1.countDown()
+ }
+
+ equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)
+ assertThat(editText.pendingCredentialCallback).isInstanceOf(OutcomeReceiver::class.java)
+
+ val passwordCredential = PasswordCredential("id", "password")
+ val frameworkPasswordResponse =
+ android.credentials.GetCredentialResponse(
+ Credential(passwordCredential.type, passwordCredential.data)
+ )
+
+ editText.pendingCredentialCallback!!.onResult(frameworkPasswordResponse)
+ latch1.await(50L, TimeUnit.MILLISECONDS)
+
+ assertThat(getCredentialResponse.get()).isNotNull()
+ val expectedGetCredentialResponse =
+ convertGetResponseToJetpackClass(frameworkPasswordResponse)
+ equals(expectedGetCredentialResponse, getCredentialResponse.get())
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
new file mode 100644
index 0000000..3d43bfd6
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.filters.SdkSuppress;
+
+import kotlin.Unit;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+public class PendingGetCredentialRequestJavaTest {
+ @Test
+ @RequiresApi(35)
+ public void constructor_setAndGetRequestThroughViewTag() {
+ GetCredentialRequest request = new GetCredentialRequest.Builder()
+ .setCredentialOptions(Collections.singletonList(new GetPasswordOption()))
+ .build();
+ PendingGetCredentialRequest pendingGetCredentialRequest =
+ new PendingGetCredentialRequest(request,
+ (response) -> Unit.INSTANCE);
+
+ assertThat(pendingGetCredentialRequest.getRequest())
+ .isSameInstanceAs(request);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
new file mode 100644
index 0000000..1e3dad7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.credentials
+
+import androidx.annotation.RequiresApi
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+class PendingGetCredentialRequestTest {
+
+ @Test
+ @RequiresApi(35)
+ fun constructor_setAndGetRequestThroughViewTag() {
+ val request =
+ GetCredentialRequest.Builder().setCredentialOptions(listOf(GetPasswordOption())).build()
+
+ val pendingGetCredentialRequest =
+ PendingGetCredentialRequest(request) { _: GetCredentialResponse? -> }
+
+ assertThat(pendingGetCredentialRequest.request).isSameInstanceAs(request)
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
index c486318..37479ec 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
@@ -16,10 +16,16 @@
package androidx.credentials
+import android.content.pm.SigningInfo
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
+import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.ProviderCreateCredentialRequest
+import androidx.credentials.provider.ProviderGetCredentialRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
/** True if the two Bundles contain the same elements, and false otherwise. */
@Suppress("DEPRECATION")
@@ -85,3 +91,115 @@
fun equals(a: CallingAppInfo, b: CallingAppInfo): Boolean {
return a.packageName == b.packageName && a.origin == b.origin
}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+ createCredentialRequest: android.service.credentials.CreateCredentialRequest,
+ request: ProviderCreateCredentialRequest
+) {
+ assertThat(createCredentialRequest.type).isEqualTo(request.callingRequest.type)
+ equals(createCredentialRequest.data, request.callingRequest.credentialData)
+ Assert.assertEquals(
+ createCredentialRequest.callingAppInfo.packageName,
+ request.callingAppInfo.packageName
+ )
+ Assert.assertEquals(
+ createCredentialRequest.callingAppInfo.origin,
+ request.callingAppInfo.origin
+ )
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+ getCredentialRequest: android.service.credentials.GetCredentialRequest,
+ request: ProviderGetCredentialRequest
+) {
+ Assert.assertEquals(
+ getCredentialRequest.callingAppInfo.packageName,
+ request.callingAppInfo.packageName
+ )
+ Assert.assertEquals(getCredentialRequest.callingAppInfo.origin, request.callingAppInfo.origin)
+ equals(getCredentialRequest.credentialOptions, request.credentialOptions)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private fun equals(
+ credentialOptions: List<android.credentials.CredentialOption>,
+ credentialOptions1: List<CredentialOption>
+) {
+ assertThat(credentialOptions.size).isEqualTo(credentialOptions1.size)
+ for (i in credentialOptions.indices) {
+ equals(credentialOptions[i], credentialOptions1[i])
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+ frameworkRequest1: android.credentials.GetCredentialRequest,
+ frameworkRequest2: android.credentials.GetCredentialRequest
+) {
+ equals(frameworkRequest1.data, frameworkRequest2.data)
+ credentialOptionsEqual(frameworkRequest1.credentialOptions, frameworkRequest2.credentialOptions)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private fun credentialOptionsEqual(
+ credentialOptions1: List<android.credentials.CredentialOption>,
+ credentialOptions2: List<android.credentials.CredentialOption>
+) {
+ assertThat(credentialOptions1.size).isEqualTo(credentialOptions2.size)
+ for (i in credentialOptions1.indices) {
+ equals(credentialOptions1[i], credentialOptions2[i])
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+ credentialOption: android.credentials.CredentialOption,
+ credentialOption1: CredentialOption
+) {
+ assertThat(credentialOption.type).isEqualTo(credentialOption1.type)
+ assertThat(credentialOption.isSystemProviderRequired)
+ .isEqualTo(credentialOption1.isSystemProviderRequired)
+ equals(credentialOption.credentialRetrievalData, credentialOption1.requestData)
+ equals(credentialOption.candidateQueryData, credentialOption1.candidateQueryData)
+ assertThat(credentialOption.allowedProviders).isEqualTo(credentialOption1.allowedProviders)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun setUpCreatePasswordRequest(): android.service.credentials.CreateCredentialRequest {
+ val passwordReq: CreateCredentialRequest =
+ CreatePasswordRequest("test-user-id", "test-password")
+ val request =
+ android.service.credentials.CreateCredentialRequest(
+ android.service.credentials.CallingAppInfo("calling_package", SigningInfo()),
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ passwordReq.credentialData
+ )
+ return request
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+ credentialOption1: android.credentials.CredentialOption,
+ credentialOption2: android.credentials.CredentialOption
+) {
+ equals(credentialOption1.candidateQueryData, credentialOption2.candidateQueryData)
+ equals(credentialOption1.credentialRetrievalData, credentialOption2.credentialRetrievalData)
+ assertThat(credentialOption1.type).isEqualTo(credentialOption2.type)
+ assertThat(credentialOption1.allowedProviders).isEqualTo(credentialOption2.allowedProviders)
+ assertThat(credentialOption1.isSystemProviderRequired)
+ .isEqualTo(credentialOption2.isSystemProviderRequired)
+}
+
+fun equals(
+ getCredentialResponse1: GetCredentialResponse,
+ getCredentialResponse2: GetCredentialResponse
+) {
+ equals(getCredentialResponse1.credential, getCredentialResponse2.credential)
+}
+
+fun equals(credential1: Credential, credential2: Credential) {
+ assertThat(credential1.type).isEqualTo(credential2.type)
+ equals(credential1.data, credential2.data)
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
new file mode 100644
index 0000000..1e5593c
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2024 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.credentials.provider;
+
+import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_ALLOWED_AUTHENTICATORS;
+import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_CRYPTO_OP_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+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 javax.crypto.NullCipher;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 35)
+public class BiometricPromptDataJavaTest {
+
+ private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = new
+ BiometricPrompt.CryptoObject(new NullCipher());
+
+ private static final long DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L;
+
+ private static final int TEST_ALLOWED_AUTHENTICATOR = BiometricManager.Authenticators
+ .BIOMETRIC_STRONG;
+
+ @Test
+ public void construct_cryptoObjectStrongAllowedAuthenticator_success() {
+ BiometricPromptData biometricPromptData = new BiometricPromptData(
+ /*cryptoObject=*/TEST_CRYPTO_OBJECT,
+ /*allowedAuthenticators=*/TEST_ALLOWED_AUTHENTICATOR
+ );
+
+ assertThat(biometricPromptData.getAllowedAuthenticators())
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+ assertThat(biometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
+ }
+
+ @Test
+ public void construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
+ int expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+ BiometricPromptData biometricPromptData = new BiometricPromptData.Builder().build();
+
+ assertThat(biometricPromptData.getCryptoObject()).isNull();
+ assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(expectedAuthenticator);
+ }
+
+ @Test
+ public void construct_cryptoObjectExistsAuthenticatorNotProvided__defaultThrowsIAE() {
+ assertThrows("Expected cryptoObject without strong authenticator to throw "
+ + "IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new BiometricPromptData.Builder()
+ .setCryptoObject(TEST_CRYPTO_OBJECT).build()
+ );
+ }
+
+ @Test
+ public void construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
+ BiometricPromptData biometricPromptData = new BiometricPromptData(
+ /*cryptoObject=*/null,
+ /*allowedAuthenticator=*/TEST_ALLOWED_AUTHENTICATOR
+ );
+
+ assertThat(biometricPromptData.getCryptoObject()).isNull();
+ assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(
+ TEST_ALLOWED_AUTHENTICATOR);
+ }
+
+ @Test
+ public void construct_authenticatorNotAccepted_throwsIAE() {
+ assertThrows("Expected invalid allowed authenticator to throw "
+ + "IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new BiometricPromptData(
+ /*cryptoObject=*/null,
+ /*allowedAuthenticator=*/Integer.MIN_VALUE
+ )
+ );
+ }
+
+ @Test
+ public void build_requiredParamsOnly_success() {
+ int expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder().build();
+
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+ expectedAllowedAuthenticators);
+ assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
+ }
+
+ @Test
+ public void build_setCryptoObjectWithStrongAuthenticator_success() {
+ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
+ .setCryptoObject(TEST_CRYPTO_OBJECT)
+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
+
+ assertThat(actualBiometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+ }
+
+ @Test
+ public void build_setAllowedAuthenticator_success() {
+ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
+
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+ }
+
+ @SdkSuppress(maxSdkVersion = 34)
+ @Test
+ public void fromBundle_validAllowedAuthenticator_success() {
+ Bundle inputBundle = new Bundle();
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
+
+ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+ assertThat(actualBiometricPromptData).isNotNull();
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+ TEST_ALLOWED_AUTHENTICATOR);
+ assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 35)
+ @Test
+ public void fromBundle_validAllowedAuthenticatorAboveApi35_success() {
+ int expectedOpId = Integer.MIN_VALUE;
+ Bundle inputBundle = new Bundle();
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+ assertThat(actualBiometricPromptData).isNotNull();
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+ TEST_ALLOWED_AUTHENTICATOR);
+ assertThat(actualBiometricPromptData.getCryptoObject()).isNotNull();
+ assertThat(actualBiometricPromptData.getCryptoObject().hashCode())
+ .isEqualTo(expectedOpId);
+ }
+
+ @Test
+ public void fromBundle_unrecognizedAllowedAuthenticator_success() {
+ int expectedOpId = Integer.MIN_VALUE;
+ Bundle inputBundle = new Bundle();
+ int unrecognizedAuthenticator = Integer.MAX_VALUE;
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator);
+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+ assertThat(actualBiometricPromptData).isNotNull();
+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+ .isEqualTo(unrecognizedAuthenticator);
+
+ }
+
+ @Test
+ public void fromBundle_invalidBundleKey_nullBiometricPromptData() {
+ int expectedOpId = Integer.MIN_VALUE;
+ Bundle inputBundle = new Bundle();
+ int unrecognizedAuthenticator = Integer.MAX_VALUE;
+ inputBundle.putInt("invalidKey", unrecognizedAuthenticator);
+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+ assertThat(actualBiometricPromptData).isNull();
+ }
+
+ @SdkSuppress(maxSdkVersion = 34)
+ @Test
+ public void toBundle_success() {
+ BiometricPromptData testBiometricPromptData = new BiometricPromptData(/*cryptoObject=*/null,
+ TEST_ALLOWED_AUTHENTICATOR);
+
+ Bundle actualBundle = BiometricPromptData.toBundle(
+ testBiometricPromptData);
+
+ assertThat(actualBundle).isNotNull();
+ assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
+ TEST_ALLOWED_AUTHENTICATOR
+ );
+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(
+ DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID);
+ }
+
+ @SdkSuppress(minSdkVersion = 35)
+ @Test
+ public void toBundle_api35AndAboveWithOpId_success() {
+ BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,
+ TEST_ALLOWED_AUTHENTICATOR);
+ long expectedOpId = TEST_CRYPTO_OBJECT.hashCode();
+
+ Bundle actualBundle = BiometricPromptData.toBundle(
+ testBiometricPromptData);
+
+ assertThat(actualBundle).isNotNull();
+ assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
+ TEST_ALLOWED_AUTHENTICATOR
+ );
+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);
+ }
+
+ @Test
+ public void build_setInvalidAllowedAuthenticator_throwsIAE() {
+ assertThrows("Expected invalid allowed authenticator to throw "
+ + "IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
+ );
+ }
+
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
new file mode 100644
index 0000000..399a783
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import android.os.Bundle
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID
+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 javax.crypto.NullCipher
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 35)
+@SmallTest
+class BiometricPromptDataTest {
+ @Test
+ fun construct_cryptoObjectStrongAllowedAuthenticator_success() {
+ val biometricPromptData =
+ BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+
+ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ assertThat(biometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
+ }
+
+ @Test
+ fun construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
+ val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK
+
+ val biometricPromptData = BiometricPromptData()
+
+ assertThat(biometricPromptData.cryptoObject).isNull()
+ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
+ }
+
+ @Test
+ fun construct_cryptoObjectExistsAuthenticatorNotProvided_defaultWeakAuthenticatorThrowsIAE() {
+ assertThrows(
+ "Expected invalid allowed authenticator with cryptoObject to throw " +
+ "IllegalArgumentException",
+ java.lang.IllegalArgumentException::class.java
+ ) {
+ BiometricPromptData(TEST_CRYPTO_OBJECT)
+ }
+ }
+
+ @Test
+ fun construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
+ val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_STRONG
+
+ val biometricPromptData = BiometricPromptData(cryptoObject = null, expectedAuthenticator)
+
+ assertThat(biometricPromptData.cryptoObject).isNull()
+ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
+ }
+
+ @Test
+ fun construct_authenticatorNotAccepted_throwsIAE() {
+ assertThrows(
+ "Expected invalid allowed authenticator IllegalArgumentException",
+ java.lang.IllegalArgumentException::class.java
+ ) {
+ BiometricPromptData(null, allowedAuthenticators = Int.MIN_VALUE)
+ }
+ }
+
+ @Test
+ fun build_requiredParamsOnly_success() {
+ val expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+
+ val actualBiometricPromptData = BiometricPromptData.Builder().build()
+
+ assertThat(actualBiometricPromptData.allowedAuthenticators)
+ .isEqualTo(expectedAllowedAuthenticators)
+ assertThat(actualBiometricPromptData.cryptoObject).isNull()
+ }
+
+ @Test
+ fun build_setCryptoObjectWithStrongAuthenticatorOnly_success() {
+ val actualBiometricPromptData =
+ BiometricPromptData.Builder()
+ .setCryptoObject(TEST_CRYPTO_OBJECT)
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
+
+ assertThat(actualBiometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
+ assertThat(actualBiometricPromptData.allowedAuthenticators)
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ }
+
+ @Test
+ fun build_setAllowedAuthenticator_success() {
+ val actualBiometricPromptData =
+ BiometricPromptData.Builder()
+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR)
+ .build()
+
+ assertThat(actualBiometricPromptData.allowedAuthenticators)
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ }
+
+ @Test
+ fun build_setInvalidAllowedAuthenticator_success() {
+ assertThrows(
+ "Expected builder invalid allowed authenticator to throw " + "IllegalArgumentException",
+ java.lang.IllegalArgumentException::class.java
+ ) {
+ BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = 34)
+ @Test
+ fun fromBundle_validAllowedAuthenticator_success() {
+ val inputBundle = Bundle()
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
+
+ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+ assertThat(actualBiometricPromptData).isNotNull()
+ assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ assertThat(actualBiometricPromptData.cryptoObject).isNull()
+ }
+
+ @SdkSuppress(minSdkVersion = 35)
+ @Test
+ fun fromBundle_validAllowedAuthenticatorAboveApi35_success() {
+ val expectedOpId = Integer.MIN_VALUE
+ val inputBundle = Bundle()
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
+
+ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+ assertThat(actualBiometricPromptData).isNotNull()
+ assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ assertThat(actualBiometricPromptData.cryptoObject).isNotNull()
+ assertThat(actualBiometricPromptData.cryptoObject!!.hashCode()).isEqualTo(expectedOpId)
+ }
+
+ @Test
+ fun fromBundle_unrecognizedAllowedAuthenticator_success() {
+ val inputBundle = Bundle()
+ val unrecognizedAuthenticator = Integer.MAX_VALUE
+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator)
+
+ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+ assertThat(actualBiometricPromptData).isNotNull()
+ assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(unrecognizedAuthenticator)
+ }
+
+ @Test
+ fun fromBundle_invalidBundleKey_nullBiometricPromptData() {
+ val expectedOpId = Integer.MIN_VALUE
+ val inputBundle = Bundle()
+ val unrecognizedAuthenticator = Integer.MAX_VALUE
+ inputBundle.putInt("invalid key", unrecognizedAuthenticator)
+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
+
+ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+ assertThat(actualBiometricPromptData).isNull()
+ }
+
+ @SdkSuppress(maxSdkVersion = 34)
+ @Test
+ fun toBundle_success() {
+ val testBiometricPromptData =
+ BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+
+ val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
+
+ assertThat(actualBundle).isNotNull()
+ assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS))
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID))
+ .isEqualTo(DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID)
+ }
+
+ @SdkSuppress(minSdkVersion = 35)
+ @Test
+ fun toBundle_api35AndAboveWithOpId_success() {
+ val testBiometricPromptData =
+ BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+ val expectedOpId = TEST_CRYPTO_OBJECT.hashCode()
+
+ val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
+
+ assertThat(actualBundle).isNotNull()
+ assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS))
+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)
+ }
+
+ private companion object {
+ private val TEST_CRYPTO_OBJECT = BiometricPrompt.CryptoObject(NullCipher())
+
+ private const val DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L
+
+ private const val TEST_ALLOWED_AUTHENTICATOR =
+ BiometricManager.Authenticators.BIOMETRIC_STRONG
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
index baaf9ee..1b9d081 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
@@ -25,6 +25,7 @@
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
@@ -192,7 +193,8 @@
packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
- signingInfo = packageInfo.signingInfo
+ assertNotNull(packageInfo.signingInfo)
+ signingInfo = packageInfo.signingInfo!!
} catch (_: PackageManager.NameNotFoundException) {}
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
index 5b332ac..2a3d871 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
@@ -18,15 +18,24 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
import android.content.Intent;
+import android.content.pm.SigningInfo;
+import android.credentials.CredentialOption;
import android.os.Build;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
import androidx.annotation.RequiresApi;
import androidx.credentials.CreatePasswordResponse;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.PasswordCredential;
+import androidx.credentials.TestUtilsKt;
import androidx.credentials.exceptions.CreateCredentialInterruptedException;
import androidx.credentials.exceptions.GetCredentialInterruptedException;
+import androidx.credentials.provider.utils.EntryUtilsKt;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
@@ -34,6 +43,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Collections;
+
@RequiresApi(34)
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -41,6 +53,244 @@
public class PendingIntentHandlerJavaTest {
private static final Intent BLANK_INTENT = new Intent();
+ private static final android.credentials.CredentialOption
+ GET_CREDENTIAL_OPTION = new CredentialOption.Builder(
+ "type", new Bundle(), new Bundle())
+ .build();
+
+ private static final android.service.credentials.GetCredentialRequest
+ GET_CREDENTIAL_REQUEST = new android.service.credentials.GetCredentialRequest(
+ new CallingAppInfo(
+ "package_name", new SigningInfo()), new ArrayList<>(
+ Collections.singleton(GET_CREDENTIAL_OPTION)));
+
+ private static final int BIOMETRIC_AUTHENTICATOR_TYPE = 1;
+
+ private static final int BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5;
+
+ private static final String BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error";
+
+ @Test
+ public void test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+ for (int jetpackResult :
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(new AuthenticationResult(jetpackResult));
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+ Intent intent = prepareIntentWithCreateRequest(
+ request,
+ biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(request);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertNotNull(biometricPromptResult.getAuthenticationResult());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+ .getAuthenticationType(), jetpackResult);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+ for (int jetpackResult :
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(new AuthenticationResult(jetpackResult));
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+
+ ProviderGetCredentialRequest retrievedRequest =
+ PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+ .getAuthenticationType(), jetpackResult);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_resultConverted() {
+ for (int frameworkResult :
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(
+ AuthenticationResult.Companion.createFrom$credentials_debug(
+ frameworkResult,
+ /*isFrameworkBiometricPrompt=*/true
+ ));
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+ int expectedResult =
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+ .get(frameworkResult);
+ Intent intent = prepareIntentWithCreateRequest(
+ request,
+ biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(request);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertNotNull(biometricPromptResult.getAuthenticationResult());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+ .getAuthenticationType(), expectedResult);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_resultConverted() {
+ for (int frameworkResult :
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(
+ AuthenticationResult.Companion.createFrom$credentials_debug(
+ frameworkResult,
+ /*isFrameworkBiometricPrompt=*/true
+ ));
+ int expectedResult =
+ AuthenticationResult.Companion
+ .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+ .get(frameworkResult);
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+
+ ProviderGetCredentialRequest retrievedRequest =
+ PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+ .getAuthenticationType(), expectedResult);
+ }
+ }
+
+
+ @Test
+ public void test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+ for (int jetpackError :
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(
+ new AuthenticationError(
+ jetpackError,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+ Intent intent = prepareIntentWithCreateRequest(
+ request, biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+ .getErrorCode(), jetpackError);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+ for (int jetpackError :
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+ BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+ new AuthenticationError(
+ jetpackError,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+
+ ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+ assertEquals(
+ retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+ .getErrorCode(), jetpackError);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_errorConverted() {
+ for (int frameworkError :
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(
+ AuthenticationError.Companion.createFrom$credentials_debug(
+ frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+ /*isFrameworkBiometricPrompt=*/true
+ ));
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+ int expectedErrorCode =
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+ .get(frameworkError);
+ Intent intent = prepareIntentWithCreateRequest(
+ request, biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+ assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+ .getErrorCode(), expectedErrorCode);
+ }
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedErr() {
+ for (int frameworkError :
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+ BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+ AuthenticationError.Companion.createFrom$credentials_debug(
+ frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+ /*isFrameworkBiometricPrompt=*/true
+ ));
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+ int expectedErrorCode =
+ AuthenticationError.Companion
+ .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+ .get(frameworkError);
+
+ ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+ assertEquals(
+ retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+ .getErrorCode(), expectedErrorCode);
+ }
+ }
+
@Test
public void test_setGetCreateCredentialException() {
if (Build.VERSION.SDK_INT >= 34) {
@@ -157,6 +407,126 @@
}
@Test
+ public void test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
+ BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+ new AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE));
+
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+
+ Intent intent = prepareIntentWithCreateRequest(request,
+ biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ }
+
+ @Test
+ public void test_retrieveProviderCreateCredReqWithFailureBpAuth() {
+ BiometricPromptResult biometricPromptResult =
+ new BiometricPromptResult(
+ new AuthenticationError(
+ BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+ android.service.credentials.CreateCredentialRequest request =
+ TestUtilsKt.setUpCreatePasswordRequest();
+ Intent intent = prepareIntentWithCreateRequest(
+ request, biometricPromptResult);
+
+ ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+ .retrieveProviderCreateCredentialRequest(intent);
+
+ assertNotNull(retrievedRequest);
+ TestUtilsKt.equals(request, retrievedRequest);
+ assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
+ BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+ new AuthenticationResult(
+ BIOMETRIC_AUTHENTICATOR_TYPE));
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+
+ ProviderGetCredentialRequest request = PendingIntentHandler
+ .retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(request);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
+ assertEquals(biometricPromptResult, request.getBiometricPromptResult());
+ }
+
+ @Test
+ public void test_retrieveProviderGetCredReqWithFailingBpAuth() {
+ BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+ new AuthenticationError(
+ BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+ Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+ biometricPromptResult);
+
+ ProviderGetCredentialRequest request = PendingIntentHandler
+ .retrieveProviderGetCredentialRequest(intent);
+
+ assertNotNull(request);
+ TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
+ assertEquals(biometricPromptResult, request.getBiometricPromptResult());
+ }
+
+ private Intent prepareIntentWithGetRequest(
+ android.service.credentials.GetCredentialRequest request,
+ BiometricPromptResult biometricPromptResult
+ ) {
+ Intent intent = new Intent();
+ intent.putExtra(CredentialProviderService
+ .EXTRA_GET_CREDENTIAL_REQUEST, request);
+ prepareIntentWithBiometricResult(intent, biometricPromptResult);
+ return intent;
+ }
+
+ private Intent prepareIntentWithCreateRequest(
+ android.service.credentials.CreateCredentialRequest request,
+ BiometricPromptResult biometricPromptResult) {
+ Intent intent = new Intent();
+ intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
+ request);
+ prepareIntentWithBiometricResult(intent, biometricPromptResult);
+ return intent;
+ }
+
+ private void prepareIntentWithBiometricResult(Intent intent,
+ BiometricPromptResult biometricPromptResult) {
+ String buildId = Build.ID;
+ if (biometricPromptResult.isSuccessful()) {
+ assertNotNull(biometricPromptResult.getAuthenticationResult());
+ String extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE;
+ if (EntryUtilsKt.requiresSlicePropertiesWorkaround()) {
+ extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK;
+ }
+ intent.putExtra(extraResultKey,
+ biometricPromptResult.getAuthenticationResult().getAuthenticationType());
+ } else {
+ assertNotNull(biometricPromptResult.getAuthenticationError());
+ String extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR;
+ String extraErrorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE;
+ if (EntryUtilsKt.requiresSlicePropertiesWorkaround()) {
+ extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK;
+ extraErrorMessageKey = AuthenticationError
+ .EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK;
+ }
+ intent.putExtra(extraErrorKey,
+ biometricPromptResult.getAuthenticationError().getErrorCode());
+ intent.putExtra(extraErrorMessageKey,
+ biometricPromptResult.getAuthenticationError().getErrorMsg());
+ }
+ }
+
+ @Test
public void test_createCredentialCredentialResponse() {
if (Build.VERSION.SDK_INT >= 34) {
return;
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
index c9ed499..af32819 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
@@ -16,17 +16,27 @@
package androidx.credentials.provider
import android.content.Intent
+import android.content.pm.SigningInfo
+import android.credentials.CredentialOption
import android.os.Build
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import android.service.credentials.CreateCredentialRequest
+import android.service.credentials.GetCredentialRequest
import androidx.annotation.RequiresApi
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
+import androidx.credentials.equals
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
+import androidx.credentials.setUpCreatePasswordRequest
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.Assert
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +45,238 @@
@RequiresApi(34)
@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
class PendingIntentHandlerTest {
+ companion object {
+ private val GET_CREDENTIAL_OPTION =
+ CredentialOption.Builder("type", Bundle(), Bundle()).build()
+
+ private val GET_CREDENTIAL_REQUEST =
+ GetCredentialRequest(
+ CallingAppInfo("package_name", SigningInfo()),
+ ArrayList(setOf(GET_CREDENTIAL_OPTION))
+ )
+
+ private const val BIOMETRIC_AUTHENTICATOR_TYPE = 1
+
+ private const val BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5
+
+ private const val BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error"
+
+ private const val FRAMEWORK_EXPECTED_CONSTANT_ERROR_CODE =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
+
+ private const val FRAMEWORK_EXPECTED_CONSTANT_ERROR_MESSAGE =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
+
+ private const val FRAMEWORK_EXPECTED_CONSTANT_AUTH_RESULT =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_RESULT"
+ }
+
+ @Test
+ fun test_constantsMatchFrameworkExpectations_success() {
+ assertThat(AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE)
+ .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_AUTH_RESULT)
+ assertThat(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR)
+ .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_ERROR_CODE)
+ assertThat(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE)
+ .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_ERROR_MESSAGE)
+ }
+
+ @Test
+ fun test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+ for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
+ val biometricPromptResult = BiometricPromptResult(AuthenticationResult(jetpackResult))
+ val request = setUpCreatePasswordRequest()
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(request, retrievedRequest!!)
+ Assert.assertNotNull(biometricPromptResult.authenticationResult)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationResult!!.authenticationType,
+ jetpackResult
+ )
+ }
+ }
+
+ @Test
+ fun test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+ for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
+ val biometricPromptResult = BiometricPromptResult(AuthenticationResult(jetpackResult))
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(GET_CREDENTIAL_REQUEST, request!!)
+ Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+ Assert.assertEquals(
+ request.biometricPromptResult!!.authenticationResult!!.authenticationType,
+ jetpackResult
+ )
+ }
+ }
+
+ // While possible to test non-conversion logic, that would equate functionally to the normal
+ // jetpack tests as there is no validation.
+ @Test
+ fun test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
+ for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationResult.createFrom(
+ uiAuthenticationType = frameworkResult,
+ isFrameworkBiometricPrompt = true
+ )
+ )
+ val request = setUpCreatePasswordRequest()
+ val expectedResult =
+ AuthenticationResult.biometricFrameworkToJetpackResultMap[frameworkResult]
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(request, retrievedRequest!!)
+ Assert.assertNotNull(biometricPromptResult.authenticationResult)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationResult!!.authenticationType,
+ expectedResult
+ )
+ }
+ }
+
+ // While possible to test non-conversion logic, that would equate functionally to the normal
+ // jetpack tests as there is no validation.
+ @Test
+ fun test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
+ for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationResult.createFrom(
+ uiAuthenticationType = frameworkResult,
+ isFrameworkBiometricPrompt = true
+ )
+ )
+ val expectedResult =
+ AuthenticationResult.biometricFrameworkToJetpackResultMap[frameworkResult]
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(GET_CREDENTIAL_REQUEST, request!!)
+ Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+ Assert.assertEquals(
+ request.biometricPromptResult!!.authenticationResult!!.authenticationType,
+ expectedResult
+ )
+ }
+ }
+
+ @Test
+ fun test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+ for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError(jetpackError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG)
+ )
+ val request = setUpCreatePasswordRequest()
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(retrievedRequest)
+ equals(request, retrievedRequest!!)
+ Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+ jetpackError
+ )
+ }
+ }
+
+ @Test
+ fun test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+ for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError(jetpackError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG)
+ )
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(retrievedRequest)
+ equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
+ Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+ jetpackError
+ )
+ }
+ }
+
+ @Test
+ fun test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
+ for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError.createFrom(
+ uiErrorCode = frameworkError,
+ uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+ isFrameworkBiometricPrompt = true
+ )
+ )
+ val expectedErrorCode =
+ AuthenticationError.biometricFrameworkToJetpackErrorMap[frameworkError]
+ val request = setUpCreatePasswordRequest()
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(retrievedRequest)
+ equals(request, retrievedRequest!!)
+ Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+ expectedErrorCode
+ )
+ }
+ }
+
+ @Test
+ fun test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
+ for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError.createFrom(
+ uiErrorCode = frameworkError,
+ uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+ isFrameworkBiometricPrompt = true
+ )
+ )
+ val expectedErrorCode =
+ AuthenticationError.biometricFrameworkToJetpackErrorMap[frameworkError]
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(retrievedRequest)
+ equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
+ Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+ Assert.assertEquals(
+ retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+ expectedErrorCode
+ )
+ }
+ }
+
@Test
fun test_createCredentialException() {
if (Build.VERSION.SDK_INT >= 34) {
@@ -51,7 +293,7 @@
assertThat(finalException).isEqualTo(initialException)
}
- @Test()
+ @Test
fun test_createCredentialException_throwsWhenEmptyIntent() {
if (Build.VERSION.SDK_INT >= 34) {
return
@@ -62,6 +304,127 @@
}
@Test
+ fun test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
+ val biometricPromptResult =
+ BiometricPromptResult(AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE))
+ val request = setUpCreatePasswordRequest()
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(request, retrievedRequest!!)
+ Assert.assertNotNull(biometricPromptResult.authenticationResult)
+ }
+
+ @Test
+ fun test_retrieveProviderCreateCredReqWithFailureBpAuth() {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError(
+ BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG
+ )
+ )
+ val request = setUpCreatePasswordRequest()
+ val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+ val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ Assert.assertNotNull(retrievedRequest)
+ equals(request, retrievedRequest!!)
+ Assert.assertEquals(biometricPromptResult, retrievedRequest.biometricPromptResult)
+ }
+
+ @Test
+ fun test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
+ val biometricPromptResult =
+ BiometricPromptResult(AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE))
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(GET_CREDENTIAL_REQUEST, request!!)
+ Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+ }
+
+ @Test
+ fun test_retrieveProviderGetCredReqWithFailingBpAuth() {
+ val biometricPromptResult =
+ BiometricPromptResult(
+ AuthenticationError(
+ BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+ BIOMETRIC_AUTHENTICATOR_ERROR_MSG
+ )
+ )
+ val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+ val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ Assert.assertNotNull(request)
+ equals(GET_CREDENTIAL_REQUEST, request!!)
+ Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+ }
+
+ private fun prepareIntentWithGetRequest(
+ request: GetCredentialRequest,
+ biometricPromptResult: BiometricPromptResult
+ ): Intent {
+ val intent = Intent()
+ intent.putExtra(
+ android.service.credentials.CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+ request
+ )
+ prepareIntentWithBiometricResult(intent, biometricPromptResult)
+ return intent
+ }
+
+ private fun prepareIntentWithCreateRequest(
+ request: CreateCredentialRequest,
+ biometricPromptResult: BiometricPromptResult
+ ): Intent {
+ val intent = Intent()
+ intent.putExtra(
+ android.service.credentials.CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
+ request
+ )
+ prepareIntentWithBiometricResult(intent, biometricPromptResult)
+ return intent
+ }
+
+ private fun prepareIntentWithBiometricResult(
+ intent: Intent,
+ biometricPromptResult: BiometricPromptResult
+ ) {
+ if (biometricPromptResult.isSuccessful) {
+ Assert.assertNotNull(biometricPromptResult.authenticationResult)
+ var extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE
+ if (requiresSlicePropertiesWorkaround()) {
+ extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK
+ }
+ intent.putExtra(
+ extraResultKey,
+ biometricPromptResult.authenticationResult!!.authenticationType
+ )
+ } else {
+ Assert.assertNotNull(biometricPromptResult.authenticationError)
+ var extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR
+ var extraErrorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE
+ if (requiresSlicePropertiesWorkaround()) {
+ extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK
+ extraErrorMessageKey =
+ AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK
+ }
+ intent.putExtra(extraErrorKey, biometricPromptResult.authenticationError!!.errorCode)
+ intent.putExtra(
+ extraErrorMessageKey,
+ biometricPromptResult.authenticationError!!.errorMsg
+ )
+ }
+ }
+
+ @Test
fun test_credentialException() {
if (Build.VERSION.SDK_INT >= 34) {
return
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
index 3f86607..22d51e6 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
@@ -22,7 +22,6 @@
import static org.junit.Assert.assertThrows;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
@@ -80,9 +79,10 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void fromSlice_success() {
Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
- Slice slice = Action.toSlice(originalAction);
+ android.app.slice.Slice slice = Action.toSlice(originalAction);
Action fromSlice = Action.fromSlice(slice);
@@ -94,9 +94,10 @@
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromAction_success() {
Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
- Slice slice = Action.toSlice(originalAction);
+ android.app.slice.Slice slice = Action.toSlice(originalAction);
Action action = Action.fromAction(new android.service.credentials.Action(slice));
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
index d628df5..d5a75c5 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
@@ -22,7 +22,6 @@
import static org.junit.Assert.assertThrows;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
@@ -75,9 +74,10 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void fromSlice_success() {
AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
- Slice slice = AuthenticationAction.toSlice(originalAction);
+ android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
AuthenticationAction fromSlice = AuthenticationAction.fromSlice(slice);
@@ -87,9 +87,10 @@
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromAction_success() {
AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
- Slice slice = AuthenticationAction.toSlice(originalAction);
+ android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
assertNotNull(slice);
AuthenticationAction action = AuthenticationAction.fromAction(
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
index 0de5cc2..83a2d1d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
@@ -23,12 +23,17 @@
import static org.junit.Assert.assertThrows;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.CreateEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -40,8 +45,10 @@
import java.time.Instant;
+import javax.crypto.NullCipher;
+
@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
public class CreateEntryJavaTest {
private static final CharSequence ACCOUNT_NAME = "account_name";
@@ -73,7 +80,17 @@
}
@Test
- public void constructor_allParameters_success() {
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ public void constructor_allParametersAboveApiO_success() {
+ CreateEntry entry = constructEntryWithAllParams();
+
+ assertNotNull(entry);
+ assertEntryWithAllParams(entry);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ public void constructor_allParametersApiOAndBelow_success() {
CreateEntry entry = constructEntryWithAllParams();
assertNotNull(entry);
@@ -128,9 +145,10 @@
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromCreateEntry_allParams_success() {
CreateEntry originalEntry = constructEntryWithAllParams();
- Slice slice = CreateEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = CreateEntry.toSlice(originalEntry);
assertNotNull(slice);
CreateEntry entry = CreateEntry.fromCreateEntry(
@@ -147,18 +165,22 @@
private void assertEntryWithRequiredParams(CreateEntry entry) {
assertThat(ACCOUNT_NAME.equals(entry.getAccountName()));
assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ assertThat(entry.getBiometricPromptData()).isNull();
}
private CreateEntry constructEntryWithAllParams() {
- return new CreateEntry.Builder(
+ CreateEntry.Builder testBuilder = new CreateEntry.Builder(
ACCOUNT_NAME,
mPendingIntent)
.setIcon(ICON)
.setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
.setPasswordCredentialCount(PASSWORD_COUNT)
.setPublicKeyCredentialCount(PUBLIC_KEY_CREDENTIAL_COUNT)
- .setTotalCredentialCount(TOTAL_COUNT)
- .build();
+ .setTotalCredentialCount(TOTAL_COUNT);
+ if (BuildCompat.isAtLeastV()) {
+ testBuilder.setBiometricPromptData(testBiometricPromptData());
+ }
+ return testBuilder.build();
}
private void assertEntryWithAllParams(CreateEntry entry) {
@@ -169,5 +191,25 @@
assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount());
assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount());
assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount());
+ if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+ assertAboveApiV(entry);
+ } else {
+ assertThat(entry.getBiometricPromptData()).isNull();
+ }
+ }
+
+ private static void assertAboveApiV(CreateEntry entry) {
+ if (BuildCompat.isAtLeastV()) {
+ assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+ testBiometricPromptData().getAllowedAuthenticators());
+ }
+ }
+
+ @RequiresApi(35)
+ private static BiometricPromptData testBiometricPromptData() {
+ return new BiometricPromptData.Builder()
+ .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build();
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
index 606a8fa..4c6810e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
@@ -20,6 +20,12 @@
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Icon
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CreateEntry.Companion.fromCreateEntry
import androidx.credentials.provider.CreateEntry.Companion.fromSlice
@@ -28,8 +34,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.time.Instant
+import javax.crypto.NullCipher
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -38,7 +45,7 @@
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
class CreateEntryTest {
private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -74,7 +81,8 @@
}
@Test
- fun constructor_allParameters_success() {
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ fun constructor_allParametersAboveApiO_success() {
val entry = constructEntryWithAllParams()
assertNotNull(entry)
@@ -139,33 +147,55 @@
}
private fun assertEntryWithRequiredParams(entry: CreateEntry) {
- Truth.assertThat(ACCOUNT_NAME == entry.accountName)
- Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ assertThat(ACCOUNT_NAME == entry.accountName)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ assertThat(entry.biometricPromptData).isNull()
}
private fun constructEntryWithAllParams(): CreateEntry {
- return CreateEntry(
- ACCOUNT_NAME,
- mPendingIntent,
- DESCRIPTION,
- Instant.ofEpochMilli(LAST_USED_TIME),
- ICON,
- PASSWORD_COUNT,
- PUBLIC_KEY_CREDENTIAL_COUNT,
- TOTAL_COUNT,
- AUTO_SELECT_BIT
- )
+ if (BuildCompat.isAtLeastV()) {
+ return CreateEntry(
+ ACCOUNT_NAME,
+ mPendingIntent,
+ DESCRIPTION,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ PASSWORD_COUNT,
+ PUBLIC_KEY_CREDENTIAL_COUNT,
+ TOTAL_COUNT,
+ AUTO_SELECT_BIT,
+ testBiometricPromptData()
+ )
+ } else {
+ return CreateEntry(
+ ACCOUNT_NAME,
+ mPendingIntent,
+ DESCRIPTION,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ PASSWORD_COUNT,
+ PUBLIC_KEY_CREDENTIAL_COUNT,
+ TOTAL_COUNT,
+ AUTO_SELECT_BIT
+ )
+ }
}
private fun assertEntryWithAllParams(entry: CreateEntry) {
- Truth.assertThat(ACCOUNT_NAME).isEqualTo(entry.accountName)
- Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
- Truth.assertThat(ICON).isEqualTo(entry.icon)
- Truth.assertThat(LAST_USED_TIME).isEqualTo(entry.lastUsedTime?.toEpochMilli())
- Truth.assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount())
- Truth.assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount())
- Truth.assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount())
- Truth.assertThat(AUTO_SELECT_BIT).isTrue()
+ assertThat(ACCOUNT_NAME).isEqualTo(entry.accountName)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ assertThat(ICON).isEqualTo(entry.icon)
+ assertThat(LAST_USED_TIME).isEqualTo(entry.lastUsedTime?.toEpochMilli())
+ assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount())
+ assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount())
+ assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount())
+ assertThat(AUTO_SELECT_BIT).isTrue()
+ if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+ assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+ } else {
+ assertThat(entry.biometricPromptData).isNull()
+ }
}
companion object {
@@ -178,5 +208,13 @@
private const val LAST_USED_TIME = 10L
private val ICON =
Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+
+ @RequiresApi(35)
+ private fun testBiometricPromptData(): BiometricPromptData {
+ return BiometricPromptData.Builder()
+ .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
index cd6ab67..d93cd88 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
@@ -25,7 +25,6 @@
import static org.junit.Assert.assertTrue;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -33,10 +32,15 @@
import android.os.Bundle;
import android.service.credentials.CredentialEntry;
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetCredentialOption;
import androidx.credentials.provider.BeginGetCustomCredentialOption;
+import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.CustomCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -47,8 +51,11 @@
import org.junit.runner.RunWith;
import java.time.Instant;
+
+import javax.crypto.NullCipher;
+
@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
public class CustomCredentialEntryJavaTest {
private static final CharSequence TITLE = "title";
@@ -149,6 +156,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 28)
public void builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
CustomCredentialEntry entry = constructEntryWithRequiredParams();
@@ -180,9 +188,11 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void fromSlice_requiredParams_success() {
CustomCredentialEntry originalEntry = constructEntryWithRequiredParams();
- Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = CustomCredentialEntry
+ .toSlice(originalEntry);
CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(
slice);
assertNotNull(entry);
@@ -190,18 +200,21 @@
}
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void fromSlice_allParams_success() {
CustomCredentialEntry originalEntry = constructEntryWithAllParams();
- Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = CustomCredentialEntry
+ .toSlice(originalEntry);
CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(slice);
assertNotNull(entry);
assertEntryWithAllParamsFromSlice(entry);
}
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromCredentialEntry_allParams_success() {
CustomCredentialEntry originalEntry = constructEntryWithAllParams();
- Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = CustomCredentialEntry.toSlice(originalEntry);
assertNotNull(slice);
CustomCredentialEntry entry = CustomCredentialEntry.fromCredentialEntry(
new CredentialEntry("id", slice));
@@ -220,11 +233,12 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
CustomCredentialEntry entry = new CustomCredentialEntry
.Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption).build();
- Slice slice = CustomCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
assertNotNull(slice);
@@ -237,12 +251,13 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_customIconSetFromSlice_returnsTrue() {
CustomCredentialEntry entry = new CustomCredentialEntry
.Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption)
.setIcon(ICON).build();
- Slice slice = CustomCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
assertNotNull(slice);
@@ -291,7 +306,7 @@
).build();
}
private CustomCredentialEntry constructEntryWithAllParams() {
- return new CustomCredentialEntry.Builder(
+ CustomCredentialEntry.Builder testBuilder = new CustomCredentialEntry.Builder(
mContext,
TYPE,
TITLE,
@@ -302,8 +317,12 @@
.setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
.setTypeDisplayName(TYPE_DISPLAY_NAME)
.setEntryGroupId(ENTRY_GROUP_ID)
- .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
- .build();
+ .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
+
+ if (BuildCompat.isAtLeastV()) {
+ testBuilder.setBiometricPromptData(testBiometricPromptData());
+ }
+ return testBuilder.build();
}
private void assertEntryWithRequiredParams(CustomCredentialEntry entry) {
assertThat(TITLE.equals(entry.getTitle()));
@@ -313,6 +332,7 @@
assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
DEFAULT_SINGLE_PROVIDER_ICON_BIT);
+ assertThat(entry.getBiometricPromptData()).isNull();
}
private void assertEntryWithRequiredParamsFromSlice(CustomCredentialEntry entry) {
assertThat(TITLE.equals(entry.getTitle()));
@@ -322,6 +342,7 @@
assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
DEFAULT_SINGLE_PROVIDER_ICON_BIT);
+ assertThat(entry.getBiometricPromptData()).isNull();
}
private void assertEntryWithAllParams(CustomCredentialEntry entry) {
assertThat(TITLE.equals(entry.getTitle()));
@@ -338,6 +359,12 @@
assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
SINGLE_PROVIDER_ICON_BIT);
+ if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+ assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+ testBiometricPromptData().getAllowedAuthenticators());
+ } else {
+ assertThat(entry.getBiometricPromptData()).isNull();
+ }
}
private void assertEntryWithAllParamsFromSlice(CustomCredentialEntry entry) {
assertThat(TITLE.equals(entry.getTitle()));
@@ -353,5 +380,19 @@
assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
SINGLE_PROVIDER_ICON_BIT);
+ if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+ assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+ testBiometricPromptData().getAllowedAuthenticators());
+ } else {
+ assertThat(entry.getBiometricPromptData()).isNull();
+ }
+ }
+
+ @RequiresApi(35)
+ private static BiometricPromptData testBiometricPromptData() {
+ return new BiometricPromptData.Builder()
+ .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build();
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
index 6669355..70aa96d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
@@ -22,11 +22,16 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetCredentialOption
import androidx.credentials.provider.BeginGetCustomCredentialOption
+import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.CustomCredentialEntry.Companion.fromCredentialEntry
import androidx.credentials.provider.CustomCredentialEntry.Companion.fromSlice
@@ -37,6 +42,7 @@
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
+import javax.crypto.NullCipher
import org.junit.Assert
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThrows
@@ -44,7 +50,7 @@
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
class CustomCredentialEntryTest {
private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -53,7 +59,6 @@
PendingIntent.getActivity(mContext, 0, mIntent, PendingIntent.FLAG_IMMUTABLE)
@Test
- @SdkSuppress(minSdkVersion = 28)
fun constructor_requiredParams_success() {
val entry = constructEntryWithRequiredParams()
assertNotNull(entry)
@@ -100,7 +105,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 23)
+ @SdkSuppress(minSdkVersion = 28)
fun constructor_nullIcon_defaultIconSet() {
val entry = constructEntryWithRequiredParams()
assertThat(
@@ -183,6 +188,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 28)
fun builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
val entry =
CustomCredentialEntry.Builder(mContext, TYPE, TITLE, mPendingIntent, BEGIN_OPTION)
@@ -201,6 +207,7 @@
assertThat(entry.entryGroupId).isEqualTo(TITLE)
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
+ assertThat(entry.biometricPromptData).isNull()
}
@Test
@@ -318,19 +325,36 @@
}
private fun constructEntryWithAllParams(): CustomCredentialEntry {
- return CustomCredentialEntry(
- mContext,
- TITLE,
- mPendingIntent,
- BEGIN_OPTION,
- SUBTITLE,
- TYPE_DISPLAY_NAME,
- Instant.ofEpochMilli(LAST_USED_TIME),
- ICON,
- IS_AUTO_SELECT_ALLOWED,
- ENTRY_GROUP_ID,
- SINGLE_PROVIDER_ICON_BIT
- )
+ return if (BuildCompat.isAtLeastV()) {
+ CustomCredentialEntry(
+ mContext,
+ TITLE,
+ mPendingIntent,
+ BEGIN_OPTION,
+ SUBTITLE,
+ TYPE_DISPLAY_NAME,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ ENTRY_GROUP_ID,
+ SINGLE_PROVIDER_ICON_BIT,
+ testBiometricPromptData()
+ )
+ } else {
+ CustomCredentialEntry(
+ mContext,
+ TITLE,
+ mPendingIntent,
+ BEGIN_OPTION,
+ SUBTITLE,
+ TYPE_DISPLAY_NAME,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ ENTRY_GROUP_ID,
+ SINGLE_PROVIDER_ICON_BIT
+ )
+ }
}
private fun assertEntryWithAllParams(entry: CustomCredentialEntry) {
@@ -344,6 +368,12 @@
assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
+ if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+ assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+ } else {
+ assertThat(entry.biometricPromptData).isNull()
+ }
}
private fun assertEntryWithAllParamsFromSlice(entry: CustomCredentialEntry) {
@@ -358,6 +388,12 @@
assertThat(BEGIN_OPTION.type).isEqualTo(entry.type)
assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
+ if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+ assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+ } else {
+ assertThat(entry.biometricPromptData).isNull()
+ }
}
private fun assertEntryWithRequiredParams(entry: CustomCredentialEntry) {
@@ -368,6 +404,7 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.entryGroupId).isEqualTo(TITLE)
+ assertThat(entry.biometricPromptData).isNull()
}
private fun assertEntryWithRequiredParamsFromSlice(entry: CustomCredentialEntry) {
@@ -377,6 +414,7 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.entryGroupId).isEqualTo(TITLE)
+ assertThat(entry.biometricPromptData).isNull()
}
companion object {
@@ -393,5 +431,13 @@
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
private const val ENTRY_GROUP_ID = "entryGroupId"
+
+ @RequiresApi(35)
+ private fun testBiometricPromptData(): BiometricPromptData {
+ return BiometricPromptData.Builder()
+ .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
index 639f2ad..48ab2a9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
@@ -25,7 +25,6 @@
import static org.junit.Assert.assertTrue;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -33,10 +32,15 @@
import android.os.Bundle;
import android.service.credentials.CredentialEntry;
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
import androidx.credentials.PasswordCredential;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetPasswordOption;
+import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.PasswordCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -48,8 +52,11 @@
import java.time.Instant;
import java.util.HashSet;
+
+import javax.crypto.NullCipher;
+
@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
public class PasswordCredentialEntryJavaTest {
private static final CharSequence USERNAME = "title";
@@ -95,6 +102,7 @@
@SdkSuppress(minSdkVersion = 28)
@Test
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_customIconSetFromSlice_returnsFalse() {
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
mContext,
@@ -103,7 +111,7 @@
mBeginGetPasswordOption
).setIcon(ICON).build();
- Slice slice = PasswordCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
assertNotNull(slice);
@@ -117,6 +125,7 @@
@SdkSuppress(minSdkVersion = 28)
@Test
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
mContext,
@@ -125,7 +134,7 @@
mBeginGetPasswordOption
).build();
- Slice slice = PasswordCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
assertNotNull(slice);
PasswordCredentialEntry entryFromSlice = PasswordCredentialEntry
.fromSlice(slice);
@@ -223,16 +232,20 @@
@Test
public void build_isAutoSelectAllowedDefault_false() {
PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
assertFalse(entry.isAutoSelectAllowed());
}
@Test
public void constructor_defaultAffiliatedDomain() {
PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
assertThat(entry.getAffiliatedDomain()).isNull();
}
+
@Test
public void constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
String expectedAffiliatedDomain = "non-empty";
+
PasswordCredentialEntry entryWithAffiliatedDomain = new PasswordCredentialEntry(
mContext,
USERNAME,
@@ -245,39 +258,47 @@
expectedAffiliatedDomain,
false
);
+
assertThat(entryWithAffiliatedDomain.getAffiliatedDomain())
.isEqualTo(expectedAffiliatedDomain);
}
@Test
+ @SdkSuppress(minSdkVersion = 34)
public void builder_constructDefault_containsOnlyDefaultValuesForSettableParameters() {
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
mPendingIntent, mBeginGetPasswordOption).build();
+
assertThat(entry.getAffiliatedDomain()).isNull();
assertThat(entry.getDisplayName()).isNull();
assertThat(entry.getLastUsedTime()).isNull();
assertThat(entry.isAutoSelectAllowed()).isFalse();
assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+ assertThat(entry.getBiometricPromptData()).isNull();
}
@Test
public void builder_setAffiliatedDomainNull_retrieveNullAffiliatedDomain() {
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
mPendingIntent, mBeginGetPasswordOption).setAffiliatedDomain(null).build();
+
assertThat(entry.getAffiliatedDomain()).isNull();
}
@Test
public void builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
String expectedAffiliatedDomain = "affiliated-domain";
+
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
mContext,
USERNAME,
mPendingIntent,
mBeginGetPasswordOption
).setAffiliatedDomain(expectedAffiliatedDomain).build();
+
assertThat(entry.getAffiliatedDomain()).isEqualTo(expectedAffiliatedDomain);
}
@Test
public void builder_setPreferredDefaultIconBit_retrieveSetIconBit() {
boolean expectedPreferredDefaultIconBit = SINGLE_PROVIDER_ICON_BIT;
+
PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
mContext,
USERNAME,
@@ -285,6 +306,7 @@
mBeginGetPasswordOption
).setDefaultIconPreferredAsSingleProvider(expectedPreferredDefaultIconBit)
.build();
+
assertThat(entry.isDefaultIconPreferredAsSingleProvider())
.isEqualTo(expectedPreferredDefaultIconBit);
}
@@ -323,20 +345,31 @@
mPendingIntent,
mBeginGetPasswordOption).build();
}
+
private PasswordCredentialEntry constructEntryWithAllParams() {
- return new PasswordCredentialEntry.Builder(
- mContext,
- USERNAME,
- mPendingIntent,
- mBeginGetPasswordOption)
- .setDisplayName(DISPLAYNAME)
- .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
- .setIcon(ICON)
- .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
- .setAffiliatedDomain(AFFILIATED_DOMAIN)
- .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
- .build();
+ if (BuildCompat.isAtLeastV()) {
+ return new PasswordCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
+ .setDisplayName(DISPLAYNAME)
+ .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+ .setIcon(ICON)
+ .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+ .setAffiliatedDomain(AFFILIATED_DOMAIN)
+ .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
+ .setBiometricPromptData(testBiometricPromptData()).build();
+ } else {
+ return new PasswordCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
+ .setDisplayName(DISPLAYNAME)
+ .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+ .setIcon(ICON)
+ .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+ .setAffiliatedDomain(AFFILIATED_DOMAIN)
+ .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
+ .build();
+ }
}
+
private void assertEntryWithRequiredParamsOnly(PasswordCredentialEntry entry,
Boolean assertOptionIdOnly) {
assertThat(USERNAME.equals(entry.getUsername()));
@@ -346,6 +379,7 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
DEFAULT_SINGLE_PROVIDER_ICON_BIT);
assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+ assertThat(entry.getBiometricPromptData()).isNull();
}
private void assertEntryWithAllParams(PasswordCredentialEntry entry) {
assertThat(USERNAME.equals(entry.getUsername()));
@@ -360,5 +394,19 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
SINGLE_PROVIDER_ICON_BIT);
assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+ if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+ assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+ testBiometricPromptData().getAllowedAuthenticators());
+ } else {
+ assertThat(entry.getBiometricPromptData()).isNull();
+ }
+ }
+
+ @RequiresApi(35)
+ private static BiometricPromptData testBiometricPromptData() {
+ return new BiometricPromptData.Builder()
+ .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build();
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
index 83b0853..bd72745 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
@@ -22,11 +22,16 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.PasswordCredential
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry.Companion.fromSlice
import androidx.test.core.app.ApplicationProvider
@@ -35,6 +40,7 @@
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
+import javax.crypto.NullCipher
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import org.junit.Assert
@@ -43,7 +49,7 @@
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
class PasswordCredentialEntryTest {
private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -182,12 +188,21 @@
@Test
fun constructor_defaultAffiliatedDomain() {
val defaultEntry = constructEntryWithRequiredParamsOnly()
+
assertThat(defaultEntry.affiliatedDomain).isNull()
}
@Test
+ fun constructor_defaultBiometricPromptData() {
+ val defaultEntry = constructEntryWithRequiredParamsOnly()
+
+ assertThat(defaultEntry.biometricPromptData).isNull()
+ }
+
+ @Test
fun constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
val expectedAffiliatedDomain = "non-empty"
+
val entryWithAffiliationType =
PasswordCredentialEntry(
mContext,
@@ -199,6 +214,7 @@
ICON,
affiliatedDomain = expectedAffiliatedDomain
)
+
assertThat(entryWithAffiliationType.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
}
@@ -216,6 +232,7 @@
ICON,
isDefaultIconPreferredAsSingleProvider = expectedPreferredDefaultIconBit
)
+
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(expectedPreferredDefaultIconBit)
}
@@ -223,6 +240,7 @@
@Test
fun constructor_preferredIconBitNotProvided_retrieveDefaultPreferredIconBit() {
val entry = PasswordCredentialEntry(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
+
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
}
@@ -252,6 +270,7 @@
assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
assertThat(entry.affiliatedDomain).isNull()
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ assertThat(entry.biometricPromptData).isNull()
}
@Test
@@ -260,16 +279,19 @@
PasswordCredentialEntry.Builder(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
.setAffiliatedDomain(null)
.build()
+
assertThat(entry.affiliatedDomain).isNull()
}
@Test
fun builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
val expectedAffiliatedDomain = "name"
+
val entry =
PasswordCredentialEntry.Builder(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
.setAffiliatedDomain(expectedAffiliatedDomain)
.build()
+
assertThat(entry.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
}
@@ -304,18 +326,34 @@
}
private fun constructEntryWithAllParams(): PasswordCredentialEntry {
- return PasswordCredentialEntry(
- mContext,
- USERNAME,
- mPendingIntent,
- BEGIN_OPTION,
- DISPLAYNAME,
- LAST_USED_TIME,
- ICON,
- IS_AUTO_SELECT_ALLOWED,
- AFFILIATED_DOMAIN,
- SINGLE_PROVIDER_ICON_BIT
- )
+ return if (BuildCompat.isAtLeastV()) {
+ PasswordCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ LAST_USED_TIME,
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ AFFILIATED_DOMAIN,
+ SINGLE_PROVIDER_ICON_BIT,
+ testBiometricPromptData(),
+ )
+ } else {
+ PasswordCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ LAST_USED_TIME,
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ AFFILIATED_DOMAIN,
+ SINGLE_PROVIDER_ICON_BIT,
+ )
+ }
}
private fun assertEntryWithRequiredParamsOnly(entry: PasswordCredentialEntry) {
@@ -325,6 +363,7 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider)
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ assertThat(entry.biometricPromptData).isNull()
}
private fun assertEntryWithAllParams(entry: PasswordCredentialEntry) {
@@ -341,6 +380,13 @@
assertThat(entry.affiliatedDomain).isEqualTo(AFFILIATED_DOMAIN)
assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ if (BuildCompat.isAtLeastV()) {
+ // TODO(b/325469910) : Add cryptoObject tests once opId is retrievable
+ assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+ } else {
+ assertThat(entry.biometricPromptData).isNull()
+ }
}
companion object {
@@ -355,5 +401,13 @@
private val AFFILIATED_DOMAIN = "affiliation-name"
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
+
+ @RequiresApi(35)
+ private fun testBiometricPromptData(): BiometricPromptData {
+ return BiometricPromptData.Builder()
+ .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
index 43cbd13..fff3b7e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
@@ -25,17 +25,21 @@
import static org.junit.Assert.assertTrue;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Bundle;
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
import androidx.credentials.PublicKeyCredential;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption;
+import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.PublicKeyCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -46,8 +50,11 @@
import org.junit.runner.RunWith;
import java.time.Instant;
+
+import javax.crypto.NullCipher;
+
@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
public class PublicKeyCredentialEntryJavaTest {
private static final CharSequence USERNAME = "title";
@@ -64,8 +71,10 @@
"{\"key1\":{\"key2\":{\"key3\":\"value3\"}}}");
private final Context mContext = ApplicationProvider.getApplicationContext();
private final Intent mIntent = new Intent();
- private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+ private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0,
+ mIntent,
PendingIntent.FLAG_IMMUTABLE);
+
@Test
public void build_requiredParamsOnly_success() {
PublicKeyCredentialEntry entry = constructWithRequiredParamsOnly();
@@ -139,9 +148,10 @@
}
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromCredentialEntry_success() {
PublicKeyCredentialEntry originalEntry = constructWithAllParams();
- Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
assertNotNull(slice);
PublicKeyCredentialEntry entry = PublicKeyCredentialEntry.fromCredentialEntry(
new android.service.credentials.CredentialEntry("id", slice));
@@ -160,10 +170,11 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
.Builder(mContext, USERNAME, mPendingIntent, mBeginOption).build();
- Slice slice = PublicKeyCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
assertNotNull(slice);
@@ -175,11 +186,12 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void isDefaultIcon_customIconAfterSlice_returnsFalse() {
PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
.Builder(mContext, USERNAME, mPendingIntent, mBeginOption)
.setIcon(ICON).build();
- Slice slice = PublicKeyCredentialEntry.toSlice(entry);
+ android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
assertNotNull(slice);
@@ -222,10 +234,15 @@
mBeginOption).build();
}
private PublicKeyCredentialEntry constructWithAllParams() {
- return new PublicKeyCredentialEntry.Builder(mContext, USERNAME, mPendingIntent,
+ PublicKeyCredentialEntry.Builder testBuilder = new PublicKeyCredentialEntry
+ .Builder(mContext, USERNAME, mPendingIntent,
mBeginOption).setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED).setDisplayName(
DISPLAYNAME).setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME)).setIcon(
- ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT).build();
+ ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
+ if (BuildCompat.isAtLeastV()) {
+ testBuilder.setBiometricPromptData(testBiometricPromptData());
+ }
+ return testBuilder.build();
}
private void assertEntryWithRequiredParams(PublicKeyCredentialEntry entry) {
assertThat(USERNAME.equals(entry.getUsername()));
@@ -234,6 +251,7 @@
DEFAULT_SINGLE_PROVIDER_ICON_BIT);
assertThat(entry.getAffiliatedDomain()).isNull();
assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+ assertThat(entry.getBiometricPromptData()).isNull();
}
private void assertEntryWithAllParams(PublicKeyCredentialEntry entry) {
assertThat(USERNAME.equals(entry.getUsername()));
@@ -247,5 +265,19 @@
SINGLE_PROVIDER_ICON_BIT);
assertThat(entry.getAffiliatedDomain()).isNull();
assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+ if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+ assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+ testBiometricPromptData().getAllowedAuthenticators());
+ } else {
+ assertThat(entry.getBiometricPromptData()).isNull();
+ }
+ }
+
+ @RequiresApi(35)
+ private static BiometricPromptData testBiometricPromptData() {
+ return new BiometricPromptData.Builder()
+ .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build();
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
index ed23e72..d4e521e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
@@ -22,11 +22,16 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromSlice
@@ -37,13 +42,14 @@
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
+import javax.crypto.NullCipher
import junit.framework.TestCase.assertNotNull
import org.junit.Assert
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
@RunWith(AndroidJUnit4::class)
@SmallTest
class PublicKeyCredentialEntryTest {
@@ -143,6 +149,7 @@
assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
assertThat(entry.affiliatedDomain).isNull()
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ assertThat(entry.biometricPromptData).isNull()
}
@Test
@@ -247,17 +254,32 @@
}
private fun constructWithAllParams(): PublicKeyCredentialEntry {
- return PublicKeyCredentialEntry(
- mContext,
- USERNAME,
- mPendingIntent,
- BEGIN_OPTION,
- DISPLAYNAME,
- Instant.ofEpochMilli(LAST_USED_TIME),
- ICON,
- IS_AUTO_SELECT_ALLOWED,
- SINGLE_PROVIDER_ICON_BIT
- )
+ return if (BuildCompat.isAtLeastV()) {
+ PublicKeyCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ SINGLE_PROVIDER_ICON_BIT,
+ testBiometricPromptData()
+ )
+ } else {
+ PublicKeyCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ IS_AUTO_SELECT_ALLOWED,
+ SINGLE_PROVIDER_ICON_BIT,
+ )
+ }
}
private fun assertEntryWithRequiredParams(entry: PublicKeyCredentialEntry) {
@@ -267,6 +289,7 @@
.isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.affiliatedDomain).isNull()
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ assertThat(entry.biometricPromptData).isNull()
}
private fun assertEntryWithAllParams(entry: PublicKeyCredentialEntry) {
@@ -280,6 +303,12 @@
assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
assertThat(entry.affiliatedDomain).isNull()
assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+ if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+ assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+ .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+ } else {
+ assertThat(entry.biometricPromptData).isNull()
+ }
}
companion object {
@@ -298,5 +327,13 @@
private const val IS_AUTO_SELECT_ALLOWED = true
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
+
+ @RequiresApi(35)
+ private fun testBiometricPromptData(): BiometricPromptData {
+ return BiometricPromptData.Builder()
+ .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
index 8a44a88..fe81ce4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
@@ -22,7 +22,6 @@
import static org.junit.Assert.assertThrows;
import android.app.PendingIntent;
-import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
@@ -80,9 +79,10 @@
@Test
@SdkSuppress(minSdkVersion = 34)
+ @SuppressWarnings("deprecation")
public void fromRemoteEntry_success() {
RemoteEntry originalEntry = new RemoteEntry(mPendingIntent);
- Slice slice = RemoteEntry.toSlice(originalEntry);
+ android.app.slice.Slice slice = RemoteEntry.toSlice(originalEntry);
assertNotNull(slice);
RemoteEntry remoteEntry = RemoteEntry.fromRemoteEntry(
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
new file mode 100644
index 0000000..2635f98
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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:JvmName("CredentialManagerViewHandler")
+
+package androidx.credentials
+
+import android.os.Build
+import android.os.OutcomeReceiver
+import android.util.Log
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.credentials.internal.FrameworkImplHelper
+
+/**
+ * An extension API to the [View] class that allows setting of a [PendingGetCredentialRequest], that
+ * in-turn contains a [GetCredentialRequest], and a callback to deliver the final
+ * [GetCredentialResponse]. The associated request is invoked when the [View] is focused/clicked by
+ * the user.
+ *
+ * A typical scenario for setting this request is a login screen with a username & password field.
+ * We recommend calling the [CredentialManager.getCredential] API when this login screen loads, so
+ * that the user can be presented with a selector with all credential options to choose from. In
+ * addition, we recommend using this API to set the same [GetCredentialRequest] that is passed to
+ * [CredentialManager.getCredential], on the username & password views. With that, if the user
+ * dismisses the initial selector, and then taps on either the username or the password field, they
+ * would see the same suggestions that they saw on the selector, but now on fallback UI experiences
+ * such as keyboard suggestions or drop-down lists, depending on the device capabilities.
+ *
+ * If you have multiple views on the screen that should invoke different requests as opposed to the
+ * same, you can simply use this API to set different requests on corresponding views, and hence a
+ * different set of suggestions will appear.
+ *
+ * Note that no errors are propagated to the [PendingGetCredentialRequest.callback]. In a scenario
+ * where multiple suggestions are presented to the user as part of the keyboard suggestions for
+ * instance, it is possible that the user selects one, but the flow ends up in an error state, due
+ * to which the final [GetCredentialResponse] cannot be propagated. In that case, user will be taken
+ * back to the suggestions, and can very well select a different suggestion which would this time
+ * result in a success. The intermediate error states are not propagated to the developer, and only
+ * a final response, if any, is propagated.
+ *
+ * @property pendingGetCredentialRequest the [GetCredentialRequest] and the associated callback to
+ * be set on the view, and to be exercised when user focused on the view in question
+ */
+private const val TAG = "ViewHandler"
+
+@Suppress("NewApi")
+var View.pendingGetCredentialRequest: PendingGetCredentialRequest?
+ get() =
+ getTag(R.id.androidx_credential_pendingCredentialRequest) as? PendingGetCredentialRequest
+ set(value) {
+ setTag(R.id.androidx_credential_pendingCredentialRequest, value)
+ if (value != null) {
+ if (
+ Build.VERSION.SDK_INT >= 35 ||
+ (Build.VERSION.SDK_INT == 34 && Build.VERSION.PREVIEW_SDK_INT > 0)
+ ) {
+ Api35Impl.setPendingGetCredentialRequest(this, value.request, value.callback)
+ }
+ } else {
+ if (
+ Build.VERSION.SDK_INT >= 35 ||
+ (Build.VERSION.SDK_INT == 34 && Build.VERSION.PREVIEW_SDK_INT > 0)
+ ) {
+ Api35Impl.clearPendingGetCredentialRequest(this)
+ }
+ }
+ }
+
+@RequiresApi(35)
+private object Api35Impl {
+ fun setPendingGetCredentialRequest(
+ view: View,
+ request: GetCredentialRequest,
+ callback: (GetCredentialResponse) -> Unit,
+ ) {
+ val frameworkRequest = FrameworkImplHelper.convertGetRequestToFrameworkClass(request)
+ val frameworkCallback =
+ object :
+ OutcomeReceiver<
+ android.credentials.GetCredentialResponse,
+ android.credentials.GetCredentialException
+ > {
+ override fun onResult(response: android.credentials.GetCredentialResponse) {
+ callback.invoke(FrameworkImplHelper.convertGetResponseToJetpackClass(response))
+ }
+
+ override fun onError(error: android.credentials.GetCredentialException) {
+ Log.w(TAG, "Error: " + error.type + " , " + error.message)
+ }
+ }
+ view.setPendingCredentialRequest(frameworkRequest, frameworkCallback)
+ }
+
+ fun clearPendingGetCredentialRequest(view: View) {
+ view.clearPendingCredentialRequest()
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index bedcc62..c81075f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -148,7 +148,7 @@
val classNames = mutableListOf<String>()
if (packageInfo.services != null) {
- for (serviceInfo in packageInfo.services) {
+ for (serviceInfo in packageInfo.services!!) {
if (serviceInfo.metaData != null) {
val className = serviceInfo.metaData.getString(CREDENTIAL_PROVIDER_KEY)
if (className != null) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
new file mode 100644
index 0000000..ae97214
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 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.credentials
+
+import android.view.View
+
+/**
+ * Request to be set on an Android [View], which will be invoked when the [View] is focused/clicked
+ * by the user.
+ *
+ * Note that the [callback] only handles a final [GetCredentialResponse] and no errors are
+ * propagated to the callback.
+ *
+ * See [View.setPendingCredentialRequest] for details on how this request will be used.
+ *
+ * @property request the [GetCredentialRequest] to be invoked when a given view on which this
+ * request is set is focused
+ * @property callback the callback on which the final [GetCredentialResponse] is returned, after the
+ * user has made its selections
+ */
+class PendingGetCredentialRequest(
+ val request: GetCredentialRequest,
+ val callback: (GetCredentialResponse) -> Unit
+)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
index 9800eff..a70417d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -16,23 +16,28 @@
package androidx.credentials.internal
+import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Icon
+import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.Credential
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
import androidx.credentials.R
-@RequiresApi(23)
-internal class FrameworkImplHelper {
+@RequiresApi(34)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class FrameworkImplHelper {
companion object {
/** Take the create request's `credentialData` and add SDK specific values to it. */
- @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
@JvmStatic
- @RequiresApi(23)
fun getFinalCreateCredentialData(
request: CreateCredentialRequest,
context: Context,
@@ -56,5 +61,52 @@
)
return createCredentialData
}
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @JvmStatic
+ fun convertGetResponseToJetpackClass(
+ response: android.credentials.GetCredentialResponse
+ ): GetCredentialResponse {
+ val credential = response.credential
+ return GetCredentialResponse(Credential.createFrom(credential.type, credential.data))
+ }
+
+ @JvmStatic
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun convertGetRequestToFrameworkClass(
+ request: GetCredentialRequest
+ ): android.credentials.GetCredentialRequest {
+ val builder =
+ android.credentials.GetCredentialRequest.Builder(
+ GetCredentialRequest.toRequestDataBundle(request)
+ )
+ request.credentialOptions.forEach {
+ builder.addCredentialOption(
+ android.credentials.CredentialOption.Builder(
+ it.type,
+ it.requestData,
+ it.candidateQueryData
+ )
+ .setIsSystemProviderRequired(it.isSystemProviderRequired)
+ .setAllowedProviders(it.allowedProviders)
+ .build()
+ )
+ }
+ setOriginForGetRequest(request, builder)
+ return builder.build()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @SuppressLint("MissingPermission")
+ @VisibleForTesting
+ @JvmStatic
+ fun setOriginForGetRequest(
+ request: GetCredentialRequest,
+ builder: android.credentials.GetCredentialRequest.Builder
+ ) {
+ if (request.origin != null) {
+ builder.setOrigin(request.origin)
+ }
+ }
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
index daf3548..2957965 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
+
package androidx.credentials.provider
import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
index e473b36..fb82267 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
+
package androidx.credentials.provider
import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
new file mode 100644
index 0000000..6d71c64
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import androidx.annotation.RestrictTo
+import java.util.Objects
+import org.jetbrains.annotations.VisibleForTesting
+
+/**
+ * Error returned from the Biometric Prompt flow that is executed by
+ * [androidx.credentials.CredentialManager] after the user makes a selection on the Credential
+ * Manager account selector.
+ *
+ * @property errorCode the error code denoting what kind of error was encountered while the
+ * biometric prompt flow failed, must be one of the error codes defined in
+ * [androidx.biometric.BiometricPrompt] such as
+ * [androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE] or
+ * [androidx.biometric.BiometricPrompt.ERROR_TIMEOUT]
+ * @property errorMsg the message associated with the [errorCode] in the form that can be displayed
+ * on a UI.
+ * @see AuthenticationErrorTypes
+ */
+class AuthenticationError
+@JvmOverloads
+constructor(
+ val errorCode: @AuthenticationErrorTypes Int,
+ val errorMsg: CharSequence? = null,
+) {
+ internal companion object {
+ internal val TAG = "AuthenticationError"
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_ERROR =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK = "BIOMETRIC_AUTH_ERROR_CODE"
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK = "BIOMETRIC_AUTH_ERROR_MESSAGE"
+ // The majority of this is unexpected to be sent, or the values are equal,
+ // but should it arrive for any reason, is handled properly. This way
+ // providers can be confident the Jetpack codes alone are enough.
+ @VisibleForTesting
+ internal val biometricFrameworkToJetpackErrorMap =
+ linkedMapOf(
+ BiometricPrompt.BIOMETRIC_ERROR_CANCELED to
+ androidx.biometric.BiometricPrompt.ERROR_CANCELED,
+ BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT to
+ androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT,
+ BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE to
+ androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE,
+ BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT to
+ androidx.biometric.BiometricPrompt.ERROR_LOCKOUT,
+ BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to
+ androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT,
+ BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to
+ androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS,
+ BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to
+ androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
+ BiometricPrompt.BIOMETRIC_ERROR_NO_SPACE to
+ androidx.biometric.BiometricPrompt.ERROR_NO_SPACE,
+ BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to
+ androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED,
+ BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT to
+ androidx.biometric.BiometricPrompt.ERROR_TIMEOUT,
+ BiometricPrompt.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to
+ androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
+ BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED to
+ androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED,
+ BiometricPrompt.BIOMETRIC_ERROR_VENDOR to
+ androidx.biometric.BiometricPrompt.ERROR_VENDOR
+ // TODO(b/340334264) : Add NEGATIVE_BUTTON from FW once avail, or wrap this in
+ // a credential manager specific error.
+ )
+
+ internal fun convertFrameworkBiometricErrorToJetpack(frameworkCode: Int): Int {
+ // Ignoring getOrDefault to allow this object down to API 21
+ return if (biometricFrameworkToJetpackErrorMap.containsKey(frameworkCode)) {
+ biometricFrameworkToJetpackErrorMap[frameworkCode]!!
+ } else {
+ Log.i(TAG, "Unexpected error code, $frameworkCode, ")
+ frameworkCode
+ }
+ }
+
+ /**
+ * Generates an instance of this class, to be called by an UI consumer that calls
+ * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
+ * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
+ * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
+ * [isFrameworkBiometricPrompt].
+ *
+ * @param uiErrorCode the error code used to create this error instance, typically using the
+ * [androidx.biometric.BiometricPrompt]'s constants if conversion isn't desired, or
+ * [android.hardware.biometrics.BiometricPrompt]'s constants if conversion *is* desired.
+ * @param uiErrorMessage the message associated with the [uiErrorCode] in the form that can
+ * be displayed on a UI.
+ * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
+ * requires conversion or not, set to true by default
+ * @return an authentication error that has properly handled conversion of the err code
+ */
+ @JvmOverloads
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ internal fun createFrom(
+ uiErrorCode: Int,
+ uiErrorMessage: CharSequence,
+ isFrameworkBiometricPrompt: Boolean = true,
+ ): AuthenticationError =
+ AuthenticationError(
+ errorCode =
+ if (isFrameworkBiometricPrompt)
+ convertFrameworkBiometricErrorToJetpack(uiErrorCode)
+ else uiErrorCode,
+ errorMsg = uiErrorMessage,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other is AuthenticationError) {
+ return this.errorCode == other.errorCode && this.errorMsg == other.errorMsg
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(this.errorCode, this.errorMsg)
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
new file mode 100644
index 0000000..1bcab70
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.ERROR_CANCELED
+import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT
+import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE
+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT
+import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS
+import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE
+import androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED
+import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT
+import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS
+import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED
+import androidx.biometric.BiometricPrompt.ERROR_VENDOR
+
+/**
+ * This acts as a parameter hint for what [BiometricPrompt]'s error constants should be. You can
+ * learn more about the constants from [BiometricPrompt] to utilize best practices.
+ *
+ * @see BiometricPrompt
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+ value =
+ [
+ ERROR_CANCELED,
+ ERROR_HW_NOT_PRESENT,
+ ERROR_HW_UNAVAILABLE,
+ ERROR_LOCKOUT,
+ ERROR_LOCKOUT_PERMANENT,
+ ERROR_NO_BIOMETRICS,
+ ERROR_NO_DEVICE_CREDENTIAL,
+ ERROR_NO_SPACE,
+ ERROR_SECURITY_UPDATE_REQUIRED,
+ ERROR_TIMEOUT,
+ ERROR_UNABLE_TO_PROCESS,
+ ERROR_USER_CANCELED,
+ ERROR_VENDOR
+ ]
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticationErrorTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
new file mode 100644
index 0000000..8810199
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.credentials.provider.AuthenticationError.Companion.TAG
+import java.util.Objects
+import org.jetbrains.annotations.VisibleForTesting
+
+/**
+ * Successful result returned from the Biometric Prompt authentication flow handled by
+ * [androidx.credentials.CredentialManager].
+ *
+ * @property authenticationType the type of authentication (e.g. device credential or biometric)
+ * that was requested from and successfully provided by the user, corresponds to constants defined
+ * in [androidx.biometric.BiometricPrompt] such as
+ * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC] or
+ * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL]
+ */
+class AuthenticationResult(
+ val authenticationType: @AuthenticationResultTypes Int,
+) {
+ internal companion object {
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE =
+ "androidx.credentials.provider.BIOMETRIC_AUTH_RESULT"
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK = "BIOMETRIC_AUTH_RESULT"
+ @VisibleForTesting
+ internal val biometricFrameworkToJetpackResultMap =
+ linkedMapOf(
+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC to
+ androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL to
+ androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
+ // TODO(b/340334264) : Add TYPE_UNKNOWN once avail from fw, though unexpected unless
+ // very low API level, and may be ignored until jp only impl added in QPR, or other
+ // ctr can be used directly once avail/ready
+ )
+
+ internal fun convertFrameworkBiometricResultToJetpack(frameworkCode: Int): Int {
+ // Ignoring getOrDefault to allow this object down to API 21
+ return if (biometricFrameworkToJetpackResultMap.containsKey(frameworkCode)) {
+ biometricFrameworkToJetpackResultMap[frameworkCode]!!
+ } else {
+ Log.i(TAG, "Non framework result code, $frameworkCode, ")
+ frameworkCode
+ }
+ }
+
+ /**
+ * Generates an instance of this class, to be called by an UI consumer that calls
+ * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
+ * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
+ * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
+ * [isFrameworkBiometricPrompt].
+ *
+ * @param uiAuthenticationType the type of authentication (e.g. device credential or
+ * biometric) that was requested from and successfully provided by the user, corresponds
+ * to constants defined in [androidx.biometric.BiometricPrompt] if conversion is not
+ * desired, or in [android.hardware.biometrics.BiometricPrompt] if conversion is desired
+ * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
+ * requires conversion or not, set to true by default
+ * @return an authentication result that has properly handled conversion of the result types
+ */
+ @JvmOverloads
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ internal fun createFrom(
+ uiAuthenticationType: Int,
+ isFrameworkBiometricPrompt: Boolean = true,
+ ): AuthenticationResult =
+ AuthenticationResult(
+ authenticationType =
+ if (isFrameworkBiometricPrompt)
+ convertFrameworkBiometricResultToJetpack(uiAuthenticationType)
+ else uiAuthenticationType
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other is AuthenticationResult) {
+ return this.authenticationType == other.authenticationType
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(this.authenticationType)
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt
new file mode 100644
index 0000000..120688c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN
+
+/**
+ * This acts as a parameter hint for what [BiometricPrompt]'s result constants should be. You can
+ * learn more about the constants from [BiometricPrompt] to utilize best practices.
+ *
+ * @see BiometricPrompt
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+ value =
+ [
+ AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
+ AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
+ AUTHENTICATION_RESULT_TYPE_UNKNOWN
+ ]
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticationResultTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
new file mode 100644
index 0000000..f008628
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import android.hardware.biometrics.BiometricManager
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+
+/**
+ * This allows verification when users pass in [BiometricManager.Authenticators] constants; namely
+ * we can have a parameter hint that indicates what they should be. You can learn more about the
+ * constants from [BiometricManager.Authenticators] to utilize best practices.
+ *
+ * @see BiometricManager.Authenticators
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(value = [BIOMETRIC_STRONG, BIOMETRIC_WEAK, DEVICE_CREDENTIAL])
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticatorTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
new file mode 100644
index 0000000..c279cca
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.core.os.BuildCompat
+
+/**
+ * Biometric prompt data that can be optionally used to provide information needed for the system to
+ * show a biometric prompt directly embedded into the Credential Manager selector.
+ *
+ * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
+ * will be shown along with a biometric / device credential capture mechanism, on a single dialog,
+ * hence avoiding navigation through multiple screens. When user confirmation is retrieved through
+ * the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]
+ * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or
+ * [CredentialEntry].
+ *
+ * Note that the value of [allowedAuthenticators] together with the features of a given device,
+ * determines whether a biometric auth or a device credential mechanism will / can be shown. The
+ * value for this property is found in [Authenticators].
+ *
+ * @property allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
+ * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
+ * @property cryptoObject a crypto object to be unlocked after successful authentication; When set,
+ * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
+ * [IllegalArgumentException] is thrown
+ * @throws IllegalArgumentException if [cryptoObject] is not null, and the [allowedAuthenticators]
+ * is not set to [BIOMETRIC_STRONG]
+ * @see Authenticators
+ */
+@RequiresApi(35)
+class BiometricPromptData
+internal constructor(
+ val cryptoObject: BiometricPrompt.CryptoObject? = null,
+ val allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK,
+ private var isCreatedFromBundle: Boolean = false,
+) {
+
+ /**
+ * Biometric prompt data that can be optionally used to provide information needed for the
+ * system to show a biometric prompt directly embedded into the Credential Manager selector.
+ *
+ * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
+ * will be shown along with a biometric / device credential capture mechanism, on a single
+ * dialog, hence avoiding navigation through multiple screens. When user confirmation is
+ * retrieved through the aforementioned biometric / device capture mechanism, the
+ * [android.app.PendingIntent] associated with the entry is invoked, and the flow continues as
+ * explained in [CreateEntry] or [CredentialEntry].
+ *
+ * Note that the value of [allowedAuthenticators] together with the features of a given device,
+ * determines whether a biometric auth or a device credential mechanism will / can be shown. The
+ * value for this property is found in [Authenticators].
+ *
+ * @param allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
+ * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
+ * @param cryptoObject a crypto object to be unlocked after successful authentication; When set,
+ * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
+ * [IllegalArgumentException] is thrown
+ * @throws IllegalArgumentException if [cryptoObject] is not null, and the
+ * [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
+ * @see Authenticators
+ */
+ @JvmOverloads
+ constructor(
+ cryptoObject: BiometricPrompt.CryptoObject? = null,
+ allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK
+ ) : this(cryptoObject, allowedAuthenticators, isCreatedFromBundle = false)
+
+ init {
+ if (!isCreatedFromBundle) {
+ // This is not expected to throw for certain eligible callers who utilize the
+ // isCreatedFromBundle hidden property.
+ require(ALLOWED_AUTHENTICATOR_VALUES.contains(allowedAuthenticators)) {
+ "The allowed authenticator must be specified according to the BiometricPrompt spec."
+ }
+ }
+ if (cryptoObject != null) {
+ require(isStrongAuthenticationType(allowedAuthenticators)) {
+ "If the cryptoObject is non-null, the allowedAuthenticator value must be " +
+ "Authenticators.BIOMETRIC_STRONG."
+ }
+ }
+ }
+
+ internal companion object {
+
+ private const val TAG = "BiometricPromptData"
+
+ internal const val BUNDLE_HINT_ALLOWED_AUTHENTICATORS =
+ "androidx.credentials.provider.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"
+
+ internal const val BUNDLE_HINT_CRYPTO_OP_ID =
+ "androidx.credentials.provider.BUNDLE_HINT_CRYPTO_OP_ID"
+
+ /**
+ * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.
+ *
+ * @param bundle the [Bundle] object constructed through [toBundle] method, often
+ */
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun fromBundle(bundle: Bundle): BiometricPromptData? {
+ return try {
+ if (!bundle.containsKey(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)) {
+ throw IllegalArgumentException("Bundle lacks allowed authenticator key.")
+ }
+ if (BuildCompat.isAtLeastV()) {
+ Api35Impl.fromBundle(bundle)
+ } else {
+ ApiMinImpl.fromBundle(bundle)
+ }
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+
+ /** Returns a [Bundle] that contains the [BiometricPromptData] representation. */
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+ return if (BuildCompat.isAtLeastV()) {
+ Api35Impl.toBundle(biometricPromptData)
+ } else {
+ ApiMinImpl.toBundle(biometricPromptData)
+ }
+ }
+
+ private fun isStrongAuthenticationType(authenticationTypes: Int?): Boolean {
+ if (authenticationTypes == null) {
+ return false
+ }
+ val biometricStrength: Int = authenticationTypes and BIOMETRIC_WEAK
+ if (biometricStrength and BiometricManager.Authenticators.BIOMETRIC_STRONG.inv() != 0) {
+ return false
+ }
+ return true
+ }
+
+ private val ALLOWED_AUTHENTICATOR_VALUES =
+ setOf(
+ BIOMETRIC_STRONG,
+ BIOMETRIC_WEAK,
+ DEVICE_CREDENTIAL,
+ BIOMETRIC_STRONG or DEVICE_CREDENTIAL,
+ BIOMETRIC_WEAK or DEVICE_CREDENTIAL
+ )
+ }
+
+ /** Builder for constructing an instance of [BiometricPromptData] */
+ class Builder {
+ private var cryptoObject: CryptoObject? = null
+ private var allowedAuthenticators: Int? = null
+
+ /**
+ * Sets whether this [BiometricPromptData] should have a crypto object associated with this
+ * authentication. If opting to pass in a value, the [allowedAuthenticators] must be
+ * [BIOMETRIC_STRONG].
+ *
+ * @param cryptoObject the [CryptoObject] to be associated with this biometric
+ * authentication flow
+ */
+ fun setCryptoObject(cryptoObject: CryptoObject): Builder {
+ this.cryptoObject = cryptoObject
+ return this
+ }
+
+ /**
+ * Specifies the type(s) of authenticators that may be invoked to authenticate the user.
+ * Available authenticator types are defined in [Authenticators] and can be combined via
+ * bitwise OR. Defaults to [BIOMETRIC_WEAK].
+ *
+ * If this method is used and no authenticator of any of the specified types is available at
+ * the time an error code will be supplied as part of [android.content.Intent] that will be
+ * launched by the containing [CredentialEntry] or [CreateEntry]'s corresponding
+ * [android.app.PendingIntent].
+ *
+ * @param allowedAuthenticators A bit field representing all valid authenticator types that
+ * may be invoked by the Credential Manager selector.
+ */
+ fun setAllowedAuthenticators(allowedAuthenticators: @AuthenticatorTypes Int): Builder {
+ this.allowedAuthenticators = allowedAuthenticators
+ return this
+ }
+
+ /**
+ * Builds the [BiometricPromptData] instance.
+ *
+ * @throws IllegalArgumentException If [cryptoObject] is not null, and the
+ * [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
+ */
+ fun build(): BiometricPromptData {
+ val allowedAuthenticators = this.allowedAuthenticators ?: BIOMETRIC_WEAK
+ return BiometricPromptData(
+ cryptoObject = cryptoObject,
+ allowedAuthenticators = allowedAuthenticators,
+ )
+ }
+ }
+
+ private object ApiMinImpl {
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(
+ BUNDLE_HINT_ALLOWED_AUTHENTICATORS,
+ biometricPromptData.allowedAuthenticators
+ )
+ return bundle
+ }
+
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun fromBundle(bundle: Bundle): BiometricPromptData {
+ val biometricPromptData =
+ BiometricPromptData(
+ allowedAuthenticators = bundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS),
+ isCreatedFromBundle = true,
+ )
+ return biometricPromptData
+ }
+ }
+
+ private object Api35Impl {
+
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(
+ BUNDLE_HINT_ALLOWED_AUTHENTICATORS,
+ biometricPromptData.allowedAuthenticators
+ )
+ biometricPromptData.cryptoObject?.let {
+ bundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, it.operationHandle)
+ }
+
+ return bundle
+ }
+
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun fromBundle(bundle: Bundle): BiometricPromptData {
+ var cryptoObject: CryptoObject? = null
+ if (bundle.containsKey(BUNDLE_HINT_CRYPTO_OP_ID)) {
+ val opId = bundle.getLong(BUNDLE_HINT_CRYPTO_OP_ID)
+ cryptoObject = CryptoObject(opId)
+ }
+ val biometricPromptData =
+ BiometricPromptData(
+ allowedAuthenticators = bundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS),
+ cryptoObject = cryptoObject,
+ isCreatedFromBundle = true,
+ )
+ return biometricPromptData
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
new file mode 100644
index 0000000..3e70c7a
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 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.credentials.provider
+
+import java.util.Objects
+
+/**
+ * The result of a Biometric Prompt authentication flow, that is propagated to the provider if the
+ * provider requested for [androidx.credentials.CredentialManager] to handle the authentication
+ * flow.
+ *
+ * An instance of this class will always be part of the final provider request, either the
+ * [ProviderGetCredentialRequest] or the [ProviderCreateCredentialRequest] that the provider
+ * receives after the user selects a [CredentialEntry] or a [CreateEntry] respectively.
+ *
+ * @property isSuccessful whether the result is a success result, in which case
+ * [authenticationResult] should be non-null
+ * @property authenticationResult the result of the authentication flow, non-null if the
+ * authentication flow was successful
+ * @property authenticationError error information, non-null if the authentication flow has
+ * failured, meaning that [isSuccessful] will be false in this case
+ */
+class BiometricPromptResult
+internal constructor(
+ val authenticationResult: AuthenticationResult? = null,
+ val authenticationError: AuthenticationError? = null
+) {
+ val isSuccessful: Boolean = authenticationResult != null
+
+ /**
+ * An unsuccessful biometric prompt result, denoting that authentication has failed.
+ *
+ * @param authenticationError the error that caused the biometric prompt authentication flow to
+ * fail
+ */
+ constructor(
+ authenticationError: AuthenticationError
+ ) : this(authenticationResult = null, authenticationError = authenticationError)
+
+ /**
+ * A successful biometric prompt result, denoting that authentication has succeeded.
+ *
+ * @param authenticationResult the result after a successful biometric prompt authentication
+ * operation
+ */
+ constructor(
+ authenticationResult: AuthenticationResult
+ ) : this(authenticationResult = authenticationResult, authenticationError = null)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other is BiometricPromptResult) {
+ return this.isSuccessful == other.isSuccessful &&
+ this.authenticationResult == other.authenticationResult &&
+ this.authenticationError == other.authenticationError
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(this.isSuccessful, this.authenticationResult, this.authenticationError)
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
index 81ef2ff..e5492bb 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
+
package androidx.credentials.provider
import android.annotation.SuppressLint
@@ -29,6 +31,8 @@
import androidx.credentials.CredentialManager
import androidx.credentials.PasswordCredential
import androidx.credentials.PublicKeyCredential
+import androidx.credentials.provider.CreateEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
import java.time.Instant
import java.util.Collections
@@ -40,9 +44,27 @@
* registered. When user selects this entry, the corresponding [PendingIntent] is fired, and the
* credential creation can be completed.
*
+ * @property accountName the name of the account where the credential will be saved
+ * @property pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ * entry, must be created with a unique request code per entry, with flag
+ * [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and NOT
+ * with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+ * @property description the localized description shown on UI about where the credential is stored
+ * @property icon the icon to be displayed with this entry on the UI, must be created using
+ * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as the
+ * latter consumes more memory and may cause undefined behavior due to memory implications on
+ * internal transactions
+ * @property lastUsedTime the last time the account underlying this entry was used by the user,
+ * distinguishable up to the milli second mark only such that if two entries have the same
+ * millisecond precision, they will be considered to have been used at the same time
+ * @property isAutoSelectAllowed whether this entry should be auto selected if it is the only entry
+ * on the selector
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set to
+ * null by default, so if not opted in, the embedded biometric prompt flow will not show
* @throws IllegalArgumentException If [accountName] is empty
*/
-@RequiresApi(26)
+@RequiresApi(23)
class CreateEntry
internal constructor(
val accountName: CharSequence,
@@ -51,9 +73,9 @@
val description: CharSequence?,
val lastUsedTime: Instant?,
private val credentialCountInformationMap: MutableMap<String, Int?>,
- val isAutoSelectAllowed: Boolean
+ val isAutoSelectAllowed: Boolean,
+ val biometricPromptData: BiometricPromptData? = null,
) {
-
/**
* Creates an entry to be displayed on the selector during create flows.
*
@@ -92,17 +114,76 @@
@Suppress("AutoBoxing") totalCredentialCount: Int? = null,
isAutoSelectAllowed: Boolean = false
) : this(
- accountName,
- pendingIntent,
- icon,
- description,
- lastUsedTime,
- mutableMapOf(
- PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
- TYPE_TOTAL_CREDENTIAL to totalCredentialCount
- ),
- isAutoSelectAllowed
+ accountName = accountName,
+ pendingIntent = pendingIntent,
+ icon = icon,
+ description = description,
+ lastUsedTime = lastUsedTime,
+ credentialCountInformationMap =
+ mutableMapOf(
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
+ TYPE_TOTAL_CREDENTIAL to totalCredentialCount
+ ),
+ isAutoSelectAllowed = isAutoSelectAllowed
+ )
+
+ /**
+ * Creates an entry to be displayed on the selector during create flows.
+ *
+ * @param accountName the name of the account where the credential will be saved
+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ * entry, must be created with a unique request code per entry, with flag
+ * [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and
+ * NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+ * @param description the localized description shown on UI about where the credential is stored
+ * @param icon the icon to be displayed with this entry on the UI, must be created using
+ * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as
+ * the latter consumes more memory and may cause undefined behavior due to memory implications
+ * on internal transactions
+ * @param lastUsedTime the last time the account underlying this entry was used by the user,
+ * distinguishable up to the milli second mark only such that if two entries have the same
+ * millisecond precision, they will be considered to have been used at the same time
+ * @param passwordCredentialCount the no. of password credentials contained by the provider
+ * @param publicKeyCredentialCount the no. of public key credentials contained by the provider
+ * @param totalCredentialCount the total no. of credentials contained by the provider
+ * @param isAutoSelectAllowed whether this entry should be auto selected if it is the only entry
+ * on the selector
+ * @param biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set
+ * to null by default, so if not opted in, the embedded biometric prompt flow will not show
+ * @constructor constructs an instance of [CreateEntry]
+ * @throws IllegalArgumentException If [accountName] is empty, or if [description] is longer
+ * than 300 characters (important: make sure your descriptions across all locales are within
+ * this limit)
+ * @throws NullPointerException If [accountName] or [pendingIntent] is null
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ constructor(
+ accountName: CharSequence,
+ pendingIntent: PendingIntent,
+ description: CharSequence? = null,
+ lastUsedTime: Instant? = null,
+ icon: Icon? = null,
+ @Suppress("AutoBoxing") passwordCredentialCount: Int? = null,
+ @Suppress("AutoBoxing") publicKeyCredentialCount: Int? = null,
+ @Suppress("AutoBoxing") totalCredentialCount: Int? = null,
+ isAutoSelectAllowed: Boolean = false,
+ biometricPromptData: BiometricPromptData? = null,
+ ) : this(
+ accountName = accountName,
+ pendingIntent = pendingIntent,
+ icon = icon,
+ description = description,
+ lastUsedTime = lastUsedTime,
+ credentialCountInformationMap =
+ mutableMapOf(
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
+ TYPE_TOTAL_CREDENTIAL to totalCredentialCount
+ ),
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ biometricPromptData = biometricPromptData,
)
init {
@@ -149,7 +230,6 @@
*/
class Builder
constructor(private val accountName: CharSequence, private val pendingIntent: PendingIntent) {
-
private var credentialCountInformationMap: MutableMap<String, Int?> = mutableMapOf()
private var icon: Icon? = null
private var description: CharSequence? = null
@@ -158,6 +238,7 @@
private var publicKeyCredentialCount: Int? = null
private var totalCredentialCount: Int? = null
private var autoSelectAllowed: Boolean = false
+ private var biometricPromptData: BiometricPromptData? = null
/** Sets whether the entry should be auto-selected. The value is false by default. */
@Suppress("MissingGetterMatchingBuilder")
@@ -237,19 +318,32 @@
}
/**
+ * Sets the biometric prompt data to optionally utilize a credential manager flow that
+ * directly handles the biometric verification for you and gives you the response; set to
+ * null by default, indicating the default behavior is to not utilize this embedded
+ * biometric prompt flow.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+ this.biometricPromptData = biometricPromptData
+ return this
+ }
+
+ /**
* Builds an instance of [CreateEntry]
*
* @throws IllegalArgumentException If [accountName] is empty
*/
fun build(): CreateEntry {
return CreateEntry(
- accountName,
- pendingIntent,
- icon,
- description,
- lastUsedTime,
- credentialCountInformationMap,
- autoSelectAllowed
+ accountName = accountName,
+ pendingIntent = pendingIntent,
+ icon = icon,
+ description = description,
+ lastUsedTime = lastUsedTime,
+ credentialCountInformationMap = credentialCountInformationMap,
+ isAutoSelectAllowed = autoSelectAllowed,
+ biometricPromptData = biometricPromptData
)
}
}
@@ -263,12 +357,91 @@
}
}
- @RequiresApi(28)
- private object Api28Impl {
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private object Api35Impl {
+ private fun addToSlice(createEntry: CreateEntry, sliceBuilder: Slice.Builder) {
+ val biometricPromptData = createEntry.biometricPromptData
+ if (biometricPromptData != null) {
+ if (requiresSlicePropertiesWorkaround()) {
+ sliceBuilder.addInt(
+ biometricPromptData.allowedAuthenticators,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+ )
+ biometricPromptData.cryptoObject?.let {
+ sliceBuilder.addLong(
+ biometricPromptData.cryptoObject.operationHandle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_CRYPTO_OP_ID)
+ )
+ }
+ } else {
+ val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+ sliceBuilder.addBundle(
+ biometricBundle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+ )
+ }
+ }
+ }
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
fun toSlice(createEntry: CreateEntry): Slice {
+ val sliceBuilder = Api28Impl.addToSlice(createEntry)
+ addToSlice(createEntry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [addToSlice]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CreateEntry? {
+ val createEntry = Api28Impl.fromSlice(slice) ?: return null
+ var biometricPromptDataBundle: Bundle? = null
+ slice.items.forEach {
+ if (it.hasHint(CredentialEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+ biometricPromptDataBundle = it.bundle
+ }
+ }
+ return try {
+ CreateEntry(
+ accountName = createEntry.accountName,
+ pendingIntent = createEntry.pendingIntent,
+ icon = createEntry.icon,
+ description = createEntry.description,
+ lastUsedTime = createEntry.lastUsedTime,
+ credentialCountInformationMap = createEntry.credentialCountInformationMap,
+ isAutoSelectAllowed = createEntry.isAutoSelectAllowed,
+ biometricPromptData =
+ if (biometricPromptDataBundle != null)
+ BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+ else null
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
+ @RequiresApi(28)
+ private object Api28Impl {
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmStatic
+ fun toSlice(createEntry: CreateEntry): Slice {
+ val sliceBuilder = addToSlice(createEntry)
+ return sliceBuilder.build()
+ }
+
+ // Specific to only this create entry, but shared across API levels > P
+ fun addToSlice(createEntry: CreateEntry): Slice.Builder {
val accountName = createEntry.accountName
val icon = createEntry.icon
val description = createEntry.description
@@ -276,14 +449,12 @@
val credentialCountInformationMap = createEntry.credentialCountInformationMap
val pendingIntent = createEntry.pendingIntent
val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(SLICE_SPEC_TYPE, REVISION_ID))
-
val autoSelectAllowed =
if (createEntry.isAutoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
-
sliceBuilder.addText(accountName, /* subType= */ null, listOf(SLICE_HINT_ACCOUNT_NAME))
if (lastUsedTime != null) {
sliceBuilder.addLong(
@@ -320,7 +491,7 @@
/*subType=*/ null,
listOf(SLICE_HINT_AUTO_SELECT_ALLOWED)
)
- return sliceBuilder.build()
+ return sliceBuilder
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -359,13 +530,13 @@
}
return try {
CreateEntry(
- accountName!!,
- pendingIntent!!,
- icon,
- description,
- lastUsedTime,
- credentialCountInfo,
- autoSelectAllowed
+ accountName = accountName!!,
+ pendingIntent = pendingIntent!!,
+ icon = icon,
+ description = description,
+ lastUsedTime = lastUsedTime,
+ credentialCountInformationMap = credentialCountInfo,
+ isAutoSelectAllowed = autoSelectAllowed,
)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
@@ -411,36 +582,30 @@
companion object {
private const val TAG = "CreateEntry"
private const val DESCRIPTION_MAX_CHAR_LIMIT = 300
-
internal const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
-
private const val SLICE_HINT_ACCOUNT_NAME =
"androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
-
private const val SLICE_HINT_NOTE =
"androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
-
private const val SLICE_HINT_ICON =
"androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
-
private const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
"androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
-
private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
private const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
-
private const val SLICE_HINT_AUTO_SELECT_ALLOWED =
"androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_SELECT_ALLOWED"
-
+ private const val SLICE_HINT_BIOMETRIC_PROMPT_DATA =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA"
+ private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
+ private const val SLICE_HINT_CRYPTO_OP_ID =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_CRYPTO_OP_ID"
private const val AUTO_SELECT_TRUE_STRING = "true"
-
private const val AUTO_SELECT_FALSE_STRING = "false"
-
private const val SLICE_SPEC_TYPE = "CreateEntry"
-
private const val REVISION_ID = 1
/**
@@ -452,7 +617,9 @@
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun toSlice(createEntry: CreateEntry): Slice? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.toSlice(createEntry)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.toSlice(createEntry)
}
return null
@@ -466,7 +633,9 @@
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun fromSlice(slice: Slice): CreateEntry? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.fromSlice(slice)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.fromSlice(slice)
}
return null
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
index 55cdd7d..efddf65 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
package androidx.credentials.provider
+import android.annotation.SuppressLint
import android.app.slice.Slice
import android.os.Build
import androidx.annotation.RequiresApi
@@ -50,6 +52,9 @@
* default credential type icon when you are the only available provider; see individual
* subclasses for these default icons (e.g. for [PublicKeyCredentialEntry], it is based on
* [R.drawable.ic_password])
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set to
+ * null by default, so if not opted in, the embedded biometric prompt flow will not show
*/
abstract class CredentialEntry
internal constructor(
@@ -58,8 +63,8 @@
val entryGroupId: CharSequence,
val isDefaultIconPreferredAsSingleProvider: Boolean,
val affiliatedDomain: CharSequence? = null,
+ val biometricPromptData: BiometricPromptData? = null,
) {
-
@RequiresApi(34)
private object Api34Impl {
@JvmStatic
@@ -71,7 +76,102 @@
}
}
+ @RequiresApi(35)
+ internal object Api35Impl {
+ @JvmStatic
+ fun toSlice(entry: CredentialEntry): Slice? {
+ when (entry) {
+ is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
+ is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
+ is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+ }
+ return null
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CredentialEntry? {
+ return try {
+ when (slice.spec?.type) {
+ TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
+ TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
+ else -> CustomCredentialEntry.fromSlice(slice)!!
+ }
+ } catch (e: Exception) {
+ // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+ // password / passkey parsing attempt.
+ CustomCredentialEntry.fromSlice(slice)
+ }
+ }
+ }
+
+ @RequiresApi(28)
+ internal object Api28Impl {
+ @JvmStatic
+ fun toSlice(entry: CredentialEntry): Slice? {
+ when (entry) {
+ is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
+ is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
+ is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+ }
+ return null
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CredentialEntry? {
+ return try {
+ when (slice.spec?.type) {
+ TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
+ TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
+ else -> CustomCredentialEntry.fromSlice(slice)!!
+ }
+ } catch (e: Exception) {
+ // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+ // password / passkey parsing attempt.
+ CustomCredentialEntry.fromSlice(slice)
+ }
+ }
+ }
+
companion object {
+ internal const val TRUE_STRING = "true"
+ internal const val FALSE_STRING = "false"
+ internal const val REVISION_ID = 1
+ internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+ internal const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+ internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+ internal const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+ internal const val SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+ internal const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
+ internal const val SLICE_HINT_OPTION_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
+ internal const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
+ internal const val SLICE_HINT_DEFAULT_ICON_RES_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
+ internal const val SLICE_HINT_AFFILIATED_DOMAIN =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
+ internal const val SLICE_HINT_DEDUPLICATION_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
+ internal const val SLICE_HINT_BIOMETRIC_PROMPT_DATA =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA"
+ internal const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
+ internal const val SLICE_HINT_CRYPTO_OP_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"
/**
* Converts a framework [android.service.credentials.CredentialEntry] class to a Jetpack
@@ -94,31 +194,26 @@
}
@JvmStatic
- @RequiresApi(28)
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun fromSlice(slice: Slice): CredentialEntry? {
- return try {
- when (slice.spec?.type) {
- TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
- TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
- else -> CustomCredentialEntry.fromSlice(slice)!!
- }
- } catch (e: Exception) {
- // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
- // password / passkey parsing attempt.
- CustomCredentialEntry.fromSlice(slice)
+ return if (Build.VERSION.SDK_INT >= 35) {
+ Api35Impl.fromSlice(slice)
+ } else if (Build.VERSION.SDK_INT >= 28) {
+ Api28Impl.fromSlice(slice)
+ } else {
+ null
}
}
@JvmStatic
- @RequiresApi(28)
internal fun toSlice(entry: CredentialEntry): Slice? {
- when (entry) {
- is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
- is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
- is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+ return if (Build.VERSION.SDK_INT >= 35) {
+ Api35Impl.toSlice(entry)
+ } else if (Build.VERSION.SDK_INT >= 28) {
+ Api28Impl.toSlice(entry)
+ } else {
+ null
}
- return null
}
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
index 299c051..0b14f6a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
+
package androidx.credentials.provider
import android.annotation.SuppressLint
@@ -29,6 +31,8 @@
import androidx.annotation.RestrictTo
import androidx.credentials.CredentialOption
import androidx.credentials.R
+import androidx.credentials.provider.CustomCredentialEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
import java.time.Instant
import java.util.Collections
@@ -69,10 +73,13 @@
* this entry was created allows this entry to be auto-selected
* @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
* a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set to
+ * null by default, so if not opted in, the embedded biometric prompt flow will not show
* @throws IllegalArgumentException If [type] or [title] are empty
* @see CredentialEntry
*/
-@RequiresApi(26)
+@RequiresApi(23)
class CustomCredentialEntry
internal constructor(
override val type: String,
@@ -87,20 +94,21 @@
isDefaultIconPreferredAsSingleProvider: Boolean,
entryGroupId: CharSequence? = title,
affiliatedDomain: CharSequence? = null,
+ biometricPromptData: BiometricPromptData? = null,
autoSelectAllowedFromOption: Boolean =
CredentialOption.extractAutoSelectValue(beginGetCredentialOption.candidateQueryData),
private var isCreatedFromSlice: Boolean = false,
private var isDefaultIconFromSlice: Boolean = false,
) :
CredentialEntry(
- type,
- beginGetCredentialOption,
- entryGroupId ?: title,
+ type = type,
+ beginGetCredentialOption = beginGetCredentialOption,
+ entryGroupId = entryGroupId ?: title,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = affiliatedDomain,
+ biometricPromptData = biometricPromptData
) {
val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
@get:JvmName("hasDefaultIcon")
val hasDefaultIcon: Boolean
get() {
@@ -143,7 +151,8 @@
ReplaceWith(
"CustomCredentialEntry(context, title, pendingIntent," +
"beginGetCredentialOption, subtitle, typeDisplayName, lastUsedTime, icon, " +
- "isAutoSelectAllowed, entryGroupId, isDefaultIconPreferredAsSingleProvider)"
+ "isAutoSelectAllowed, entryGroupId, isDefaultIconPreferredAsSingleProvider," +
+ "biometricPromptData)"
),
level = DeprecationLevel.HIDDEN
)
@@ -158,6 +167,63 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
@Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
) : this(
+ type = beginGetCredentialOption.type,
+ title = title,
+ pendingIntent = pendingIntent,
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ subtitle = subtitle,
+ typeDisplayName = typeDisplayName,
+ icon = icon,
+ lastUsedTime = lastUsedTime,
+ beginGetCredentialOption = beginGetCredentialOption,
+ isDefaultIconPreferredAsSingleProvider = false
+ )
+
+ /**
+ * @param context the context of the calling app, required to retrieve fallback resources
+ * @param title the title shown with this entry on the selector UI
+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system
+ * to attach the final request
+ * @param beginGetCredentialOption the option from the original [BeginGetCredentialRequest], for
+ * which this credential entry is being added
+ * @param subtitle the subTitle shown with this entry on the selector UI
+ * @param lastUsedTime the last used time the credential underlying this entry was used by the
+ * user, distinguishable up to the milli second mark only such that if two entries have the
+ * same millisecond precision, they will be considered to have been used at the same time
+ * @param typeDisplayName the friendly name to be displayed on the UI for the type of the
+ * credential
+ * @param icon the icon to be displayed with this entry on the selector UI, if not set a default
+ * icon representing a custom credential type is set by the library
+ * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+ * only one on the UI, only takes effect if the app requesting for credentials also opts for
+ * auto select
+ * @param entryGroupId an ID to uniquely mark this entry for deduplication or to group entries
+ * during display, set to [title] by default
+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+ * default credential type icon (see the default value of [icon]) when you are the only
+ * available provider; false by default
+ * @param biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set
+ * to null by default, so if not opted in, the embedded biometric prompt flow will not show
+ * @constructor constructs an instance of [CustomCredentialEntry]
+ * @throws IllegalArgumentException If [type] or [title] are empty
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ constructor(
+ context: Context,
+ title: CharSequence,
+ pendingIntent: PendingIntent,
+ beginGetCredentialOption: BeginGetCredentialOption,
+ subtitle: CharSequence? = null,
+ typeDisplayName: CharSequence? = null,
+ lastUsedTime: Instant? = null,
+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
+ @Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
+ entryGroupId: CharSequence = title,
+ isDefaultIconPreferredAsSingleProvider: Boolean = false,
+ biometricPromptData: BiometricPromptData? = null,
+ ) : this(
beginGetCredentialOption.type,
title,
pendingIntent,
@@ -167,7 +233,9 @@
icon,
lastUsedTime,
beginGetCredentialOption,
- isDefaultIconPreferredAsSingleProvider = false
+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+ entryGroupId = entryGroupId.ifEmpty { title },
+ biometricPromptData = biometricPromptData,
)
/**
@@ -208,7 +276,7 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
@Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
entryGroupId: CharSequence = title,
- isDefaultIconPreferredAsSingleProvider: Boolean = false
+ isDefaultIconPreferredAsSingleProvider: Boolean = false,
) : this(
beginGetCredentialOption.type,
title,
@@ -220,7 +288,7 @@
lastUsedTime,
beginGetCredentialOption,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
- entryGroupId.ifEmpty { title },
+ entryGroupId = entryGroupId.ifEmpty { title },
)
@RequiresApi(34)
@@ -234,6 +302,94 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private object Api35Impl {
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmStatic
+ fun toSlice(entry: CustomCredentialEntry): Slice {
+ val type = entry.type
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ Api28Impl.addToSlice(entry, sliceBuilder)
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Given multiple API dependencies, this captures common builds across all API levels > V
+ // and across all subclasses for the toSlice method
+ fun addToSlice(entry: CustomCredentialEntry, sliceBuilder: Slice.Builder) {
+ val biometricPromptData = entry.biometricPromptData
+ if (biometricPromptData != null) {
+ if (requiresSlicePropertiesWorkaround()) {
+ sliceBuilder.addInt(
+ biometricPromptData.allowedAuthenticators,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+ )
+ biometricPromptData.cryptoObject?.let {
+ sliceBuilder.addLong(
+ biometricPromptData.cryptoObject.operationHandle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_CRYPTO_OP_ID)
+ )
+ }
+ } else {
+ val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+ sliceBuilder.addBundle(
+ biometricBundle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [addToSlice]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CustomCredentialEntry? {
+ val customCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+ var biometricPromptDataBundle: Bundle? = null
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+ biometricPromptDataBundle = it.bundle
+ }
+ }
+ return try {
+ CustomCredentialEntry(
+ type = customCredentialEntry.type,
+ title = customCredentialEntry.title,
+ pendingIntent = customCredentialEntry.pendingIntent,
+ isAutoSelectAllowed = customCredentialEntry.isAutoSelectAllowed,
+ subtitle = customCredentialEntry.subtitle,
+ typeDisplayName = customCredentialEntry.typeDisplayName,
+ icon = customCredentialEntry.icon,
+ lastUsedTime = customCredentialEntry.lastUsedTime,
+ beginGetCredentialOption = customCredentialEntry.beginGetCredentialOption,
+ isDefaultIconPreferredAsSingleProvider =
+ customCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+ entryGroupId = customCredentialEntry.entryGroupId,
+ affiliatedDomain = customCredentialEntry.affiliatedDomain,
+ autoSelectAllowedFromOption =
+ customCredentialEntry.isAutoSelectAllowedFromOption,
+ isCreatedFromSlice = true,
+ isDefaultIconFromSlice = customCredentialEntry.isDefaultIconFromSlice,
+ biometricPromptData =
+ if (biometricPromptDataBundle != null)
+ BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+ else null
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
@RequiresApi(28)
private object Api28Impl {
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -250,6 +406,41 @@
@JvmStatic
fun toSlice(entry: CustomCredentialEntry): Slice {
val type = entry.type
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Specific to only this custom credential entry, but shared across API levels > P
+ fun addToSlice(entry: CustomCredentialEntry, sliceBuilder: Slice.Builder) {
+ val beginGetCredentialOption = entry.beginGetCredentialOption
+ val entryGroupId = entry.entryGroupId
+ val isDefaultIconPreferredAsSingleProvider =
+ entry.isDefaultIconPreferredAsSingleProvider
+ val affiliatedDomain = entry.affiliatedDomain
+ val isUsingDefaultIcon =
+ if (isDefaultIconPreferredAsSingleProvider) {
+ TRUE_STRING
+ } else {
+ FALSE_STRING
+ }
+ sliceBuilder
+ .addText(
+ beginGetCredentialOption.id,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_OPTION_ID)
+ )
+ .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+ .addText(
+ isUsingDefaultIcon,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+ )
+ .addText(
+ affiliatedDomain,
+ /*subTypes=*/ null,
+ listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+ )
val title = entry.title
val subtitle = entry.subtitle
val pendingIntent = entry.pendingIntent
@@ -257,61 +448,18 @@
val lastUsedTime = entry.lastUsedTime
val icon = entry.icon
val isAutoSelectAllowed = entry.isAutoSelectAllowed
- val beginGetCredentialOption = entry.beginGetCredentialOption
- val entryGroupId = entry.entryGroupId
- val affiliatedDomain = entry.affiliatedDomain
- val isDefaultIconPreferredAsSingleProvider =
- entry.isDefaultIconPreferredAsSingleProvider
-
val autoSelectAllowed =
if (isAutoSelectAllowed) {
TRUE_STRING
} else {
FALSE_STRING
}
-
- val isUsingDefaultIconPreferred =
- if (isDefaultIconPreferredAsSingleProvider) {
- TRUE_STRING
- } else {
- FALSE_STRING
- }
- val sliceBuilder =
- Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
- .addText(
- typeDisplayName,
- /*subType=*/ null,
- listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
- )
- .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
- .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
- .addText(
- autoSelectAllowed,
- /*subType=*/ null,
- listOf(SLICE_HINT_AUTO_ALLOWED)
- )
- .addText(
- beginGetCredentialOption.id,
- /*subType=*/ null,
- listOf(SLICE_HINT_OPTION_ID)
- )
- .addText(
- entryGroupId,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_DEDUPLICATION_ID)
- )
- .addText(
- affiliatedDomain,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_AFFILIATED_DOMAIN)
- )
- .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
- .addText(
- isUsingDefaultIconPreferred,
- /*subType=*/ null,
- listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
- )
-
+ sliceBuilder
+ .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+ .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+ .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+ .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+ .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
try {
if (entry.hasDefaultIcon) {
sliceBuilder.addInt(
@@ -321,7 +469,6 @@
)
}
} catch (_: IllegalStateException) {}
-
if (entry.isAutoSelectAllowedFromOption) {
sliceBuilder.addInt(
/*true=*/ 1,
@@ -343,19 +490,22 @@
.build(),
/*subType=*/ null
)
- return sliceBuilder.build()
}
/**
* Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
*
- * @param slice the [Slice] object constructed through [toSlice]
+ * @param slice the [Slice] object constructed through [addToSlice]
*/
@RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): CustomCredentialEntry? {
val type: String = slice.spec!!.type
+ var entryGroupId: CharSequence? = null
+ var affiliatedDomain: CharSequence? = null
+ var isDefaultIconPreferredAsSingleProvider = false
+ var beginGetCredentialOptionId: CharSequence? = null
var typeDisplayName: CharSequence? = null
var title: CharSequence? = null
var subtitle: CharSequence? = null
@@ -363,15 +513,21 @@
var pendingIntent: PendingIntent? = null
var lastUsedTime: Instant? = null
var autoSelectAllowed = false
- var beginGetCredentialOptionId: CharSequence? = null
- var entryGroupId: CharSequence? = null
var autoSelectAllowedFromOption = false
- var isDefaultIconPreferredAsSingleProvider = false
var isDefaultIcon = false
- var affiliatedDomain: CharSequence? = null
-
slice.items.forEach {
- if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+ if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+ beginGetCredentialOptionId = it.text
+ } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+ entryGroupId = it.text
+ } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+ val defaultIconValue = it.text
+ if (defaultIconValue == TRUE_STRING) {
+ isDefaultIconPreferredAsSingleProvider = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+ affiliatedDomain = it.text
+ } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
typeDisplayName = it.text
} else if (it.hasHint(SLICE_HINT_TITLE)) {
title = it.text
@@ -381,8 +537,6 @@
icon = it.icon
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
- } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
- beginGetCredentialOptionId = it.text
} else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
lastUsedTime = Instant.ofEpochMilli(it.long)
} else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -390,22 +544,12 @@
if (autoSelectValue == TRUE_STRING) {
autoSelectAllowed = true
}
- } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
- entryGroupId = it.text
} else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
autoSelectAllowedFromOption = true
- } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
- val defaultIconValue = it.text
- if (defaultIconValue == TRUE_STRING) {
- isDefaultIconPreferredAsSingleProvider = true
- }
} else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
isDefaultIcon = true
- } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
- affiliatedDomain = it.text
}
}
-
return try {
CustomCredentialEntry(
type = type,
@@ -439,51 +583,6 @@
companion object {
private const val TAG = "CredentialEntry"
- private const val SLICE_HINT_TYPE_DISPLAY_NAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_TITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
- private const val SLICE_HINT_SUBTITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
- private const val SLICE_HINT_ICON =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
- private const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
- private const val SLICE_HINT_AUTO_ALLOWED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
- private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
- private const val SLICE_HINT_OPTION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
- private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
- private const val SLICE_HINT_DEDUPLICATION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
- private const val SLICE_HINT_AFFILIATED_DOMAIN =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
- private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
- private const val TRUE_STRING = "true"
-
- private const val FALSE_STRING = "false"
-
- private const val REVISION_ID = 1
-
/**
* Converts an instance of [CustomCredentialEntry] to a [Slice].
*
@@ -493,7 +592,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
fun toSlice(entry: CustomCredentialEntry): Slice? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.toSlice(entry)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.toSlice(entry)
}
return null
@@ -502,13 +603,15 @@
/**
* Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
*
- * @param slice the [Slice] object constructed through [toSlice]
+ * @param slice the [Slice] object constructed through [addToSlice]
*/
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun fromSlice(slice: Slice): CustomCredentialEntry? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.fromSlice(slice)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.fromSlice(slice)
}
return null
@@ -560,6 +663,7 @@
private var autoSelectAllowed = false
private var entryGroupId: CharSequence = title
private var isDefaultIconPreferredAsSingleProvider = false
+ private var biometricPromptData: BiometricPromptData? = null
/** Sets a displayName to be shown on the UI with this entry. */
fun setSubtitle(subtitle: CharSequence?): Builder {
@@ -582,6 +686,18 @@
return this
}
+ /**
+ * Sets the biometric prompt data to optionally utilize a credential manager flow that
+ * directly handles the biometric verification for you and gives you the response; set to
+ * null by default, indicating the default behavior is to not utilize this embedded
+ * biometric prompt flow.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+ this.biometricPromptData = biometricPromptData
+ return this
+ }
+
/** Sets whether the entry should be auto-selected. The value is false by default. */
@Suppress("MissingGetterMatchingBuilder")
fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -627,17 +743,18 @@
icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in)
}
return CustomCredentialEntry(
- type,
- title,
- pendingIntent,
- autoSelectAllowed,
- subtitle,
- typeDisplayName,
- icon!!,
- lastUsedTime,
- beginGetCredentialOption,
+ type = type,
+ title = title,
+ pendingIntent = pendingIntent,
+ isAutoSelectAllowed = autoSelectAllowed,
+ subtitle = subtitle,
+ typeDisplayName = typeDisplayName,
+ icon = icon!!,
+ lastUsedTime = lastUsedTime,
+ beginGetCredentialOption = beginGetCredentialOption,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
entryGroupId = entryGroupId,
+ biometricPromptData = biometricPromptData,
)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
index 034b0af..0d17857 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
package androidx.credentials.provider
@@ -31,7 +32,9 @@
import androidx.credentials.CredentialOption
import androidx.credentials.PasswordCredential
import androidx.credentials.R
+import androidx.credentials.provider.PasswordCredentialEntry.Api28Impl.toSlice
import androidx.credentials.provider.PasswordCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
import java.time.Instant
import java.util.Collections
@@ -64,11 +67,14 @@
* this entry was created allows this entry to be auto-selected
* @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
* a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set to
+ * null by default, so if not opted in, the embedded biometric prompt flow will not show
* @throws IllegalArgumentException If [username] is empty
* @see CustomCredentialEntry
* @see CredentialEntry
*/
-@RequiresApi(26)
+@RequiresApi(23)
class PasswordCredentialEntry
internal constructor(
val username: CharSequence,
@@ -82,21 +88,21 @@
isDefaultIconPreferredAsSingleProvider: Boolean,
entryGroupId: CharSequence? = username,
affiliatedDomain: CharSequence? = null,
+ biometricPromptData: BiometricPromptData? = null,
autoSelectAllowedFromOption: Boolean =
CredentialOption.extractAutoSelectValue(beginGetPasswordOption.candidateQueryData),
private var isCreatedFromSlice: Boolean = false,
private var isDefaultIconFromSlice: Boolean = false
) :
CredentialEntry(
- PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
- beginGetPasswordOption,
- entryGroupId ?: username,
+ type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ beginGetCredentialOption = beginGetPasswordOption,
+ entryGroupId = entryGroupId ?: username,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = affiliatedDomain,
+ biometricPromptData = biometricPromptData,
) {
-
val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
@get:JvmName("hasDefaultIcon")
val hasDefaultIcon: Boolean
get() {
@@ -185,6 +191,70 @@
* @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
* only one on the UI, only takes effect if the app requesting for credentials also opts for
* auto select
+ * @param affiliatedDomain the user visible affiliated domain, a CharSequence representation of
+ * a web domain or an app package name that the given credential in this entry is associated
+ * with when it is different from the requesting entity, default null
+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+ * default credential type icon (see the default value of [icon]) when you are the only
+ * available provider; false by default
+ * @param biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set
+ * to null by default, so if not opted in, the embedded biometric prompt flow will not show
+ * @constructor constructs an instance of [PasswordCredentialEntry]
+ *
+ * The [affiliatedDomain] parameter is filled if you provide a credential that is not directly
+ * associated with the requesting entity, but rather originates from an entity that is
+ * determined as being associated with the requesting entity through mechanisms such as digital
+ * asset links.
+ *
+ * @throws IllegalArgumentException If [username] is empty
+ * @throws NullPointerException If [context], [username], [pendingIntent], or
+ * [beginGetPasswordOption] is null
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ constructor(
+ context: Context,
+ username: CharSequence,
+ pendingIntent: PendingIntent,
+ beginGetPasswordOption: BeginGetPasswordOption,
+ displayName: CharSequence? = null,
+ lastUsedTime: Instant? = null,
+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
+ isAutoSelectAllowed: Boolean = false,
+ affiliatedDomain: CharSequence? = null,
+ isDefaultIconPreferredAsSingleProvider: Boolean = false,
+ biometricPromptData: BiometricPromptData? = null
+ ) : this(
+ username,
+ displayName,
+ typeDisplayName = context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL),
+ pendingIntent,
+ lastUsedTime,
+ icon,
+ isAutoSelectAllowed,
+ beginGetPasswordOption,
+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+ affiliatedDomain = affiliatedDomain,
+ biometricPromptData = biometricPromptData,
+ )
+
+ /**
+ * @param context the context of the calling app, required to retrieve fallback resources
+ * @param username the username of the account holding the password credential
+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system
+ * to attach the final request
+ * @param beginGetPasswordOption the option from the original [BeginGetCredentialRequest], for
+ * which this credential entry is being added
+ * @param displayName the displayName of the account holding the password credential
+ * @param lastUsedTime the last used time the credential underlying this entry was used by the
+ * user, distinguishable up to the milli second mark only such that if two entries have the
+ * same millisecond precision, they will be considered to have been used at the same time
+ * @param icon the icon to be displayed with this entry on the selector, if not set, a default
+ * icon representing a password credential type is set by the library
+ * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+ * only one on the UI, only takes effect if the app requesting for credentials also opts for
+ * auto select
* @constructor constructs an instance of [PasswordCredentialEntry]
* @throws IllegalArgumentException If [username] is empty
* @throws NullPointerException If [context], [username], [pendingIntent], or
@@ -196,7 +266,8 @@
ReplaceWith(
"PasswordCredentialEntry(context, username, " +
"pendingIntent, beginGetPasswordOption, displayName, lastUsedTime, icon, " +
- "isAutoSelectAllowed, affiliatedDomain, isDefaultIconPreferredAsSingleProvider)"
+ "isAutoSelectAllowed, affiliatedDomain, isDefaultIconPreferredAsSingleProvider, " +
+ "biometricPromptData)"
),
level = DeprecationLevel.HIDDEN
)
@@ -210,14 +281,14 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
isAutoSelectAllowed: Boolean = false
) : this(
- username,
- displayName,
+ username = username,
+ displayName = displayName,
typeDisplayName = context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL),
- pendingIntent,
- lastUsedTime,
- icon,
- isAutoSelectAllowed,
- beginGetPasswordOption,
+ pendingIntent = pendingIntent,
+ lastUsedTime = lastUsedTime,
+ icon = icon,
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ beginGetPasswordOption = beginGetPasswordOption,
isDefaultIconPreferredAsSingleProvider = false
)
@@ -232,6 +303,94 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private object Api35Impl {
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmStatic
+ fun toSlice(entry: PasswordCredentialEntry): Slice {
+ val type = entry.type
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ Api28Impl.addToSlice(entry, sliceBuilder)
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Given multiple API dependencies, this captures common builds across all API levels > V
+ // and across all subclasses for the toSlice method
+ fun addToSlice(entry: PasswordCredentialEntry, sliceBuilder: Slice.Builder) {
+ val biometricPromptData = entry.biometricPromptData
+ if (biometricPromptData != null) {
+ if (requiresSlicePropertiesWorkaround()) {
+ sliceBuilder.addInt(
+ biometricPromptData.allowedAuthenticators,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+ )
+ biometricPromptData.cryptoObject?.let {
+ sliceBuilder.addLong(
+ biometricPromptData.cryptoObject.operationHandle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_CRYPTO_OP_ID)
+ )
+ }
+ } else {
+ val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+ sliceBuilder.addBundle(
+ biometricBundle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): PasswordCredentialEntry? {
+ val passwordCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+ var biometricPromptDataBundle: Bundle? = null
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+ biometricPromptDataBundle = it.bundle
+ }
+ }
+ return try {
+ PasswordCredentialEntry(
+ username = passwordCredentialEntry.username,
+ displayName = passwordCredentialEntry.displayName,
+ typeDisplayName = passwordCredentialEntry.typeDisplayName,
+ pendingIntent = passwordCredentialEntry.pendingIntent,
+ lastUsedTime = passwordCredentialEntry.lastUsedTime,
+ icon = passwordCredentialEntry.icon,
+ isAutoSelectAllowed = passwordCredentialEntry.isAutoSelectAllowed,
+ beginGetPasswordOption =
+ passwordCredentialEntry.beginGetCredentialOption as BeginGetPasswordOption,
+ entryGroupId = passwordCredentialEntry.entryGroupId,
+ isDefaultIconPreferredAsSingleProvider =
+ passwordCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+ affiliatedDomain = passwordCredentialEntry.affiliatedDomain,
+ autoSelectAllowedFromOption =
+ passwordCredentialEntry.isAutoSelectAllowedFromOption,
+ isCreatedFromSlice = true,
+ isDefaultIconFromSlice = passwordCredentialEntry.isDefaultIconFromSlice,
+ biometricPromptData =
+ if (biometricPromptDataBundle != null)
+ BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+ else null
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
@RequiresApi(28)
private object Api28Impl {
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -248,6 +407,41 @@
@JvmStatic
fun toSlice(entry: PasswordCredentialEntry): Slice {
val type = entry.type
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Specific to only this custom credential entry, but shared across API levels > P
+ fun addToSlice(entry: PasswordCredentialEntry, sliceBuilder: Slice.Builder) {
+ val beginGetCredentialOption = entry.beginGetCredentialOption
+ val entryGroupId = entry.entryGroupId
+ val isDefaultIconPreferredAsSingleProvider =
+ entry.isDefaultIconPreferredAsSingleProvider
+ val affiliatedDomain = entry.affiliatedDomain
+ val isUsingDefaultIcon =
+ if (isDefaultIconPreferredAsSingleProvider) {
+ TRUE_STRING
+ } else {
+ FALSE_STRING
+ }
+ sliceBuilder
+ .addText(
+ beginGetCredentialOption.id,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_OPTION_ID)
+ )
+ .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+ .addText(
+ isUsingDefaultIcon,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+ )
+ .addText(
+ affiliatedDomain,
+ /*subTypes=*/ null,
+ listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+ )
val title = entry.username
val subtitle = entry.displayName
val pendingIntent = entry.pendingIntent
@@ -255,55 +449,18 @@
val lastUsedTime = entry.lastUsedTime
val icon = entry.icon
val isAutoSelectAllowed = entry.isAutoSelectAllowed
- val beginGetPasswordCredentialOption = entry.beginGetCredentialOption
- val affiliatedDomain = entry.affiliatedDomain
- val entryGroupId = entry.entryGroupId
- var isDefaultIconPreferredAsSingleProvider =
- entry.isDefaultIconPreferredAsSingleProvider
-
val autoSelectAllowed =
if (isAutoSelectAllowed) {
TRUE_STRING
} else {
FALSE_STRING
}
- val isUsingDefaultIcon =
- if (isDefaultIconPreferredAsSingleProvider) TRUE_STRING else FALSE_STRING
- val sliceBuilder =
- Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
- .addText(
- typeDisplayName,
- /*subType=*/ null,
- listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
- )
- .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
- .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
- .addText(
- autoSelectAllowed,
- /*subType=*/ null,
- listOf(SLICE_HINT_AUTO_ALLOWED)
- )
- .addText(
- beginGetPasswordCredentialOption.id,
- /*subType=*/ null,
- listOf(SLICE_HINT_OPTION_ID)
- )
- .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
- .addText(
- entryGroupId,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_DEDUPLICATION_ID)
- )
- .addText(
- affiliatedDomain,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_AFFILIATED_DOMAIN)
- )
- .addText(
- isUsingDefaultIcon,
- /*subType=*/ null,
- listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
- )
+ sliceBuilder
+ .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+ .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+ .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+ .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+ .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
try {
if (entry.hasDefaultIcon) {
sliceBuilder.addInt(
@@ -313,7 +470,6 @@
)
}
} catch (_: IllegalStateException) {}
-
if (entry.isAutoSelectAllowedFromOption) {
sliceBuilder.addInt(
/*true=*/ 1,
@@ -335,7 +491,6 @@
.build(),
/*subType=*/ null
)
- return sliceBuilder.build()
}
/**
@@ -347,33 +502,41 @@
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): PasswordCredentialEntry? {
+ var entryGroupId: CharSequence? = null
+ var affiliatedDomain: CharSequence? = null
+ var isDefaultIconPreferredAsSingleProvider = false
+ var beginGetCredentialOptionId: CharSequence? = null
var typeDisplayName: CharSequence? = null
var title: CharSequence? = null
- var subTitle: CharSequence? = null
+ var subtitle: CharSequence? = null
var icon: Icon? = null
var pendingIntent: PendingIntent? = null
var lastUsedTime: Instant? = null
var autoSelectAllowed = false
var autoSelectAllowedFromOption = false
- var beginGetPasswordOptionId: CharSequence? = null
- var isDefaultIconPreferredAsSingleProvider = false
- var affiliatedDomain: CharSequence? = null
- var entryGroupId: CharSequence? = null
var isDefaultIcon = false
-
slice.items.forEach {
- if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+ if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+ beginGetCredentialOptionId = it.text
+ } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+ entryGroupId = it.text
+ } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+ val defaultIconValue = it.text
+ if (defaultIconValue == TRUE_STRING) {
+ isDefaultIconPreferredAsSingleProvider = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+ affiliatedDomain = it.text
+ } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
typeDisplayName = it.text
} else if (it.hasHint(SLICE_HINT_TITLE)) {
title = it.text
} else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
- subTitle = it.text
+ subtitle = it.text
} else if (it.hasHint(SLICE_HINT_ICON)) {
icon = it.icon
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
- } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
- beginGetPasswordOptionId = it.text
} else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
lastUsedTime = Instant.ofEpochMilli(it.long)
} else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -383,24 +546,14 @@
}
} else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
autoSelectAllowedFromOption = true
- } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
- affiliatedDomain = it.text
- } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
- entryGroupId = it.text
- } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
- val defaultIconValue = it.text
- if (defaultIconValue == TRUE_STRING) {
- isDefaultIconPreferredAsSingleProvider = true
- }
} else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
isDefaultIcon = true
}
}
-
return try {
PasswordCredentialEntry(
username = title!!,
- displayName = subTitle,
+ displayName = subtitle,
typeDisplayName = typeDisplayName!!,
pendingIntent = pendingIntent!!,
lastUsedTime = lastUsedTime,
@@ -409,7 +562,7 @@
beginGetPasswordOption =
BeginGetPasswordOption.createFrom(
Bundle(),
- beginGetPasswordOptionId!!.toString()
+ beginGetCredentialOptionId!!.toString()
),
entryGroupId = entryGroupId,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
@@ -428,51 +581,6 @@
companion object {
private const val TAG = "PasswordCredentialEntry"
- private const val SLICE_HINT_TYPE_DISPLAY_NAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_TITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
- private const val SLICE_HINT_SUBTITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
- private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
- private const val SLICE_HINT_ICON =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
- private const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
- private const val SLICE_HINT_OPTION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
- private const val SLICE_HINT_AUTO_ALLOWED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
- private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
- private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
- private const val SLICE_HINT_DEDUPLICATION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
- private const val SLICE_HINT_AFFILIATED_DOMAIN =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
- private const val TRUE_STRING = "true"
-
- private const val FALSE_STRING = "false"
-
- private const val REVISION_ID = 1
-
/**
* Converts an instance of [PasswordCredentialEntry] to a [Slice].
*
@@ -482,7 +590,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
fun toSlice(entry: PasswordCredentialEntry): Slice? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.toSlice(entry)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.toSlice(entry)
}
return null
@@ -491,7 +601,9 @@
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun fromSlice(slice: Slice): PasswordCredentialEntry? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.fromSlice(slice)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.fromSlice(slice)
}
return null
@@ -546,6 +658,7 @@
private var autoSelectAllowed = false
private var affiliatedDomain: CharSequence? = null
private var isDefaultIconPreferredAsSingleProvider: Boolean = false
+ private var biometricPromptData: BiometricPromptData? = null
/** Sets a displayName to be shown on the UI with this entry. */
fun setDisplayName(displayName: CharSequence?): Builder {
@@ -559,6 +672,18 @@
return this
}
+ /**
+ * Sets the biometric prompt data to optionally utilize a credential manager flow that
+ * directly handles the biometric verification for you and gives you the response; set to
+ * null by default, indicating the default behavior is to not utilize this embedded
+ * biometric prompt flow.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+ this.biometricPromptData = biometricPromptData
+ return this
+ }
+
/** Sets whether the entry should be auto-selected. The value is false by default. */
@Suppress("MissingGetterMatchingBuilder")
fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -605,16 +730,17 @@
val typeDisplayName =
context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL)
return PasswordCredentialEntry(
- username,
- displayName,
- typeDisplayName,
- pendingIntent,
- lastUsedTime,
- icon!!,
- autoSelectAllowed,
- beginGetPasswordOption,
+ username = username,
+ displayName = displayName,
+ typeDisplayName = typeDisplayName,
+ pendingIntent = pendingIntent,
+ lastUsedTime = lastUsedTime,
+ icon = icon!!,
+ isAutoSelectAllowed = autoSelectAllowed,
+ beginGetPasswordOption = beginGetPasswordOption,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = affiliatedDomain,
+ biometricPromptData = biometricPromptData,
)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
index e4971fa..8af4316 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.credentials.provider
import android.app.Activity
@@ -31,6 +30,7 @@
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.provider.utils.BeginGetCredentialUtil
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
import java.util.stream.Collectors
/**
@@ -77,26 +77,69 @@
Log.i(TAG, "Request not found in pendingIntent")
return frameworkReq
}
+ val biometricPromptResult =
+ if (requiresSlicePropertiesWorkaround())
+ retrieveBiometricPromptResultFallback(intent)
+ else retrieveBiometricPromptResult(intent)
return try {
ProviderCreateCredentialRequest(
- androidx.credentials.CreateCredentialRequest.createFrom(
- frameworkReq.type,
- frameworkReq.data,
- frameworkReq.data,
- requireSystemProvider = false,
- frameworkReq.callingAppInfo.origin
- ),
- CallingAppInfo(
- frameworkReq.callingAppInfo.packageName,
- frameworkReq.callingAppInfo.signingInfo,
- frameworkReq.callingAppInfo.origin
- )
+ callingRequest =
+ androidx.credentials.CreateCredentialRequest.createFrom(
+ frameworkReq.type,
+ frameworkReq.data,
+ frameworkReq.data,
+ requireSystemProvider = false,
+ frameworkReq.callingAppInfo.origin
+ ),
+ callingAppInfo =
+ CallingAppInfo(
+ frameworkReq.callingAppInfo.packageName,
+ frameworkReq.callingAppInfo.signingInfo,
+ frameworkReq.callingAppInfo.origin
+ ),
+ biometricPromptResult = biometricPromptResult
)
} catch (e: IllegalArgumentException) {
return null
}
}
+ private fun retrieveBiometricPromptResult(
+ intent: Intent,
+ resultKey: String? = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE,
+ errorKey: String? = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR,
+ errorMessageKey: String? = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE
+ ): BiometricPromptResult? {
+ if (intent.extras == null) {
+ return null
+ }
+ if (intent.extras!!.containsKey(resultKey)) {
+ val authResultType = intent.extras!!.getInt(resultKey)
+ return BiometricPromptResult(
+ authenticationResult = AuthenticationResult(authResultType)
+ )
+ } else if (intent.extras!!.containsKey(errorKey)) {
+ val authResultError = intent.extras!!.getInt(errorKey)
+ return BiometricPromptResult(
+ authenticationError =
+ AuthenticationError(
+ authResultError,
+ intent.extras?.getCharSequence(errorMessageKey)
+ )
+ )
+ }
+ return null
+ }
+
+ private fun retrieveBiometricPromptResultFallback(intent: Intent): BiometricPromptResult? {
+ return retrieveBiometricPromptResult(
+ intent,
+ resultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK,
+ errorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK,
+ errorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK
+ )
+ }
+
/**
* Extracts the [BeginGetCredentialRequest] from the provider's [PendingIntent] invoked by
* the Android system when the user selects an [AuthenticationAction].
@@ -160,7 +203,10 @@
Log.i(TAG, "Get request from framework is null")
return null
}
-
+ val biometricPromptResult =
+ if (requiresSlicePropertiesWorkaround())
+ retrieveBiometricPromptResultFallback(intent)
+ else retrieveBiometricPromptResult(intent)
return ProviderGetCredentialRequest.createFrom(
frameworkReq.credentialOptions
.stream()
@@ -178,7 +224,8 @@
frameworkReq.callingAppInfo.packageName,
frameworkReq.callingAppInfo.signingInfo,
frameworkReq.callingAppInfo.origin
- )
+ ),
+ biometricPromptResult
)
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
index 9077238..aae640f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.credentials.provider
import androidx.credentials.CreateCredentialRequest
@@ -25,14 +24,22 @@
* This request contains the actual request coming from the calling app, and the application
* information associated with the calling app.
*
- * @param callingRequest the complete [CreateCredentialRequest] coming from the calling app that is
- * requesting for credential creation
- * @param callingAppInfo information pertaining to the calling app making the request
* @constructor constructs an instance of [ProviderCreateCredentialRequest]
+ * @property callingRequest the complete [CreateCredentialRequest] coming from the calling app that
+ * is requesting for credential creation
+ * @property callingAppInfo information pertaining to the calling app making the request
+ * @property biometricPromptResult the result of a Biometric Prompt authentication flow, that is
+ * propagated to the provider if the provider requested for
+ * [androidx.credentials.CredentialManager] to handle the authentication flow
* @throws NullPointerException If [callingRequest], or [callingAppInfo] is null
*
* Note : Credential providers are not expected to utilize the constructor in this class for any
* production flow. This constructor must only be used for testing purposes.
*/
class ProviderCreateCredentialRequest
-constructor(val callingRequest: CreateCredentialRequest, val callingAppInfo: CallingAppInfo)
+@JvmOverloads
+constructor(
+ val callingRequest: CreateCredentialRequest,
+ val callingAppInfo: CallingAppInfo,
+ val biometricPromptResult: BiometricPromptResult? = null
+)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
index f733715..ab957ed 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
@@ -27,28 +27,36 @@
* set on the [CredentialEntry] that the user selected. The request must be extracted using the
* [PendingIntentHandler.retrieveProviderGetCredentialRequest] helper API.
*
- * @param credentialOptions the list of credential retrieval options containing the required
+ * @constructor constructs an instance of [ProviderGetCredentialRequest]
+ * @property credentialOptions the list of credential retrieval options containing the required
* parameters, expected to contain a single [CredentialOption] when this request is retrieved from
* the [android.app.Activity] invoked by the [android.app.PendingIntent] set on a
* [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain multiple
* [CredentialOption] when this request is retrieved from the [android.app.Activity] invoked by
* the [android.app.PendingIntent] set on a [RemoteEntry]
- * @param callingAppInfo information pertaining to the calling application
+ * @property callingAppInfo information pertaining to the calling application
+ * @property biometricPromptResult the result of a Biometric Prompt authentication flow, that is
+ * propagated to the provider if the provider requested for
+ * [androidx.credentials.CredentialManager] to handle the authentication flow
*
* Note : Credential providers are not expected to utilize the constructor in this class for any
* production flow. This constructor must only be used for testing purposes.
- *
- * @constructor constructs an instance of [ProviderGetCredentialRequest]
*/
class ProviderGetCredentialRequest
-constructor(val credentialOptions: List<CredentialOption>, val callingAppInfo: CallingAppInfo) {
+@JvmOverloads
+constructor(
+ val credentialOptions: List<CredentialOption>,
+ val callingAppInfo: CallingAppInfo,
+ val biometricPromptResult: BiometricPromptResult? = null,
+) {
internal companion object {
@JvmStatic
internal fun createFrom(
options: List<CredentialOption>,
- callingAppInfo: CallingAppInfo
+ callingAppInfo: CallingAppInfo,
+ biometricPromptResult: BiometricPromptResult? = null
): ProviderGetCredentialRequest {
- return ProviderGetCredentialRequest(options, callingAppInfo)
+ return ProviderGetCredentialRequest(options, callingAppInfo, biometricPromptResult)
}
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
index 8d1b8f1..4ebc1f0 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
package androidx.credentials.provider
@@ -31,7 +32,9 @@
import androidx.credentials.CredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.R
+import androidx.credentials.provider.PublicKeyCredentialEntry.Api28Impl.toSlice
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
import java.time.Instant
import java.util.Collections
@@ -67,10 +70,14 @@
* this entry was created allows this entry to be auto-selected
* @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
* a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set to
+ * null by default, so if not opted in, the embedded biometric prompt flow will not show
* @throws IllegalArgumentException If [username] is empty
* @see CredentialEntry
*/
-@RequiresApi(26)
+@RequiresApi(23)
+@Suppress("DEPRECATION") // For usage of slice
class PublicKeyCredentialEntry
internal constructor(
val username: CharSequence,
@@ -84,6 +91,7 @@
isDefaultIconPreferredAsSingleProvider: Boolean,
entryGroupId: CharSequence? = username,
affiliatedDomain: CharSequence? = null,
+ biometricPromptData: BiometricPromptData? = null,
autoSelectAllowedFromOption: Boolean =
CredentialOption.extractAutoSelectValue(
beginGetPublicKeyCredentialOption.candidateQueryData
@@ -92,14 +100,14 @@
private val isDefaultIconFromSlice: Boolean = false,
) :
CredentialEntry(
- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- beginGetPublicKeyCredentialOption,
- entryGroupId ?: username,
+ type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+ beginGetCredentialOption = beginGetPublicKeyCredentialOption,
+ entryGroupId = entryGroupId ?: username,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = affiliatedDomain,
+ biometricPromptData = biometricPromptData,
) {
val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
@get:JvmName("hasDefaultIcon")
val hasDefaultIcon: Boolean
get() {
@@ -159,7 +167,61 @@
lastUsedTime,
isAutoSelectAllowed,
beginGetPublicKeyCredentialOption,
- isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+ )
+
+ /**
+ * @param context the context of the calling app, required to retrieve fallback resources
+ * @param username the username of the account holding the public key credential
+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ * entry, must be created with a unique request code per entry, with flag
+ * [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and
+ * NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+ * @param beginGetPublicKeyCredentialOption the option from the original
+ * [BeginGetCredentialRequest], for which this credential entry is being added
+ * @param displayName the displayName of the account holding the public key credential
+ * @param lastUsedTime the last used time the credential underlying this entry was used by the
+ * user, distinguishable up to the milli second mark only such that if two entries have the
+ * same millisecond precision, they will be considered to have been used at the same time
+ * @param icon the icon to be displayed with this entry on the selector, if not set, a default
+ * icon representing a public key credential type is set by the library
+ * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+ * only one on the UI, only takes effect if the app requesting for credentials also opts for
+ * auto select
+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+ * default credential type icon (see the default value of [icon]) when you are the only
+ * available provider; false by default
+ * @param biometricPromptData the data that is set optionally to utilize a credential manager
+ * flow that directly handles the biometric verification and presents back the response; set
+ * to null by default, so if not opted in, the embedded biometric prompt flow will not show
+ * @constructor constructs an instance of [PublicKeyCredentialEntry]
+ * @throws NullPointerException If [context], [username], [pendingIntent], or
+ * [beginGetPublicKeyCredentialOption] is null
+ * @throws IllegalArgumentException if [username] is empty
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ constructor(
+ context: Context,
+ username: CharSequence,
+ pendingIntent: PendingIntent,
+ beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,
+ displayName: CharSequence? = null,
+ lastUsedTime: Instant? = null,
+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
+ isAutoSelectAllowed: Boolean = false,
+ isDefaultIconPreferredAsSingleProvider: Boolean = false,
+ biometricPromptData: BiometricPromptData? = null,
+ ) : this(
+ username,
+ displayName,
+ context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
+ pendingIntent,
+ icon,
+ lastUsedTime,
+ isAutoSelectAllowed,
+ beginGetPublicKeyCredentialOption,
+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+ biometricPromptData = biometricPromptData,
)
/**
@@ -186,12 +248,12 @@
* @throws IllegalArgumentException if [username] is empty
*/
@Deprecated(
- "Use the constructor that allows setting all parameters.",
+ "Use the constructor with all parameters dependent on API levels",
replaceWith =
ReplaceWith(
"PublicKeyCredentialEntry(context, username, pendingIntent," +
"beginGetPublicKeyCredentialOption, displayName, lastUsedTime, icon, " +
- "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider)"
+ "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider, biometricPromptData)"
),
level = DeprecationLevel.HIDDEN
)
@@ -205,20 +267,20 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
isAutoSelectAllowed: Boolean = false,
) : this(
- username,
- displayName,
- context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
- pendingIntent,
- icon,
- lastUsedTime,
- isAutoSelectAllowed,
- beginGetPublicKeyCredentialOption,
+ username = username,
+ displayName = displayName,
+ typeDisplayName =
+ context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
+ pendingIntent = pendingIntent,
+ icon = icon,
+ lastUsedTime = lastUsedTime,
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
isDefaultIconPreferredAsSingleProvider = false
)
@RequiresApi(34)
private object Api34Impl {
-
@JvmStatic
fun fromCredentialEntry(
credentialEntry: android.service.credentials.CredentialEntry
@@ -228,6 +290,95 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private object Api35Impl {
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmStatic
+ fun toSlice(entry: PublicKeyCredentialEntry): Slice {
+ val type = entry.type
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ Api28Impl.addToSlice(entry, sliceBuilder)
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Given multiple API dependencies, this captures common builds across all API levels > V
+ // and across all subclasses for the toSlice method
+ fun addToSlice(entry: PublicKeyCredentialEntry, sliceBuilder: Slice.Builder) {
+ val biometricPromptData = entry.biometricPromptData
+ if (biometricPromptData != null) {
+ if (requiresSlicePropertiesWorkaround()) {
+ sliceBuilder.addInt(
+ biometricPromptData.allowedAuthenticators,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+ )
+ biometricPromptData.cryptoObject?.let {
+ sliceBuilder.addLong(
+ biometricPromptData.cryptoObject.operationHandle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_CRYPTO_OP_ID)
+ )
+ }
+ } else {
+ val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+ sliceBuilder.addBundle(
+ biometricBundle,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
+ val publicKeyCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+ var biometricPromptDataBundle: Bundle? = null
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+ biometricPromptDataBundle = it.bundle
+ }
+ }
+ return try {
+ PublicKeyCredentialEntry(
+ username = publicKeyCredentialEntry.username,
+ displayName = publicKeyCredentialEntry.displayName,
+ typeDisplayName = publicKeyCredentialEntry.typeDisplayName,
+ pendingIntent = publicKeyCredentialEntry.pendingIntent,
+ icon = publicKeyCredentialEntry.icon,
+ lastUsedTime = publicKeyCredentialEntry.lastUsedTime,
+ isAutoSelectAllowed = publicKeyCredentialEntry.isAutoSelectAllowed,
+ beginGetPublicKeyCredentialOption =
+ publicKeyCredentialEntry.beginGetCredentialOption
+ as BeginGetPublicKeyCredentialOption,
+ entryGroupId = publicKeyCredentialEntry.entryGroupId,
+ isDefaultIconPreferredAsSingleProvider =
+ publicKeyCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+ affiliatedDomain = publicKeyCredentialEntry.affiliatedDomain,
+ autoSelectAllowedFromOption =
+ publicKeyCredentialEntry.isAutoSelectAllowedFromOption,
+ isCreatedFromSlice = true,
+ isDefaultIconFromSlice = publicKeyCredentialEntry.isDefaultIconFromSlice,
+ biometricPromptData =
+ if (biometricPromptDataBundle != null)
+ BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+ else null
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
@RequiresApi(28)
private object Api28Impl {
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -244,66 +395,60 @@
@JvmStatic
fun toSlice(entry: PublicKeyCredentialEntry): Slice {
val type = entry.type
- val title = entry.username
- val subTitle = entry.displayName
- val pendingIntent = entry.pendingIntent
- val typeDisplayName = entry.typeDisplayName
- val lastUsedTime = entry.lastUsedTime
- val icon = entry.icon
- val isAutoSelectAllowed = entry.isAutoSelectAllowed
- val beginGetPublicKeyCredentialOption = entry.beginGetCredentialOption
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+ addToSlice(entry, sliceBuilder)
+ return sliceBuilder.build()
+ }
+
+ // Specific to only this custom credential entry, but shared across API levels > P
+ fun addToSlice(entry: PublicKeyCredentialEntry, sliceBuilder: Slice.Builder) {
+ val beginGetCredentialOption = entry.beginGetCredentialOption
val entryGroupId = entry.entryGroupId
- val affiliatedDomain = entry.affiliatedDomain
val isDefaultIconPreferredAsSingleProvider =
entry.isDefaultIconPreferredAsSingleProvider
-
- val autoSelectAllowed =
- if (isAutoSelectAllowed) {
- TRUE_STRING
- } else {
- FALSE_STRING
- }
+ val affiliatedDomain = entry.affiliatedDomain
val isUsingDefaultIcon =
if (isDefaultIconPreferredAsSingleProvider) {
TRUE_STRING
} else {
FALSE_STRING
}
- val sliceBuilder =
- Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
- .addText(
- typeDisplayName,
- /*subType=*/ null,
- listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
- )
- .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
- .addText(subTitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
- .addText(
- autoSelectAllowed,
- /*subType=*/ null,
- listOf(SLICE_HINT_AUTO_ALLOWED)
- )
- .addText(
- beginGetPublicKeyCredentialOption.id,
- /*subType=*/ null,
- listOf(SLICE_HINT_OPTION_ID)
- )
- .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
- .addText(
- entryGroupId,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_DEDUPLICATION_ID)
- )
- .addText(
- affiliatedDomain,
- /*subTypes=*/ null,
- listOf(SLICE_HINT_AFFILIATED_DOMAIN)
- )
- .addText(
- isUsingDefaultIcon,
- /*subType=*/ null,
- listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
- )
+ sliceBuilder
+ .addText(
+ beginGetCredentialOption.id,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_OPTION_ID)
+ )
+ .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+ .addText(
+ isUsingDefaultIcon,
+ /*subType=*/ null,
+ listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+ )
+ .addText(
+ affiliatedDomain,
+ /*subTypes=*/ null,
+ listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+ )
+ val title = entry.username
+ val subtitle = entry.displayName
+ val pendingIntent = entry.pendingIntent
+ val typeDisplayName = entry.typeDisplayName
+ val lastUsedTime = entry.lastUsedTime
+ val icon = entry.icon
+ val isAutoSelectAllowed = entry.isAutoSelectAllowed
+ val autoSelectAllowed =
+ if (isAutoSelectAllowed) {
+ TRUE_STRING
+ } else {
+ FALSE_STRING
+ }
+ sliceBuilder
+ .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+ .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+ .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+ .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+ .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
try {
if (entry.hasDefaultIcon) {
sliceBuilder.addInt(
@@ -313,7 +458,6 @@
)
}
} catch (_: IllegalStateException) {}
-
if (entry.isAutoSelectAllowedFromOption) {
sliceBuilder.addInt(
/*true=*/ 1,
@@ -335,7 +479,6 @@
.build(),
/*subType=*/ null
)
- return sliceBuilder.build()
}
/**
@@ -347,6 +490,10 @@
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
+ var entryGroupId: CharSequence? = null
+ var affiliatedDomain: CharSequence? = null
+ var isDefaultIconPreferredAsSingleProvider = false
+ var beginGetCredentialOptionId: CharSequence? = null
var typeDisplayName: CharSequence? = null
var title: CharSequence? = null
var subtitle: CharSequence? = null
@@ -354,15 +501,21 @@
var pendingIntent: PendingIntent? = null
var lastUsedTime: Instant? = null
var autoSelectAllowed = false
- var beginGetPublicKeyCredentialOptionId: CharSequence? = null
var autoSelectAllowedFromOption = false
- var isDefaultIconPreferredAsSingleProvider = false
var isDefaultIcon = false
- var entryGroupId: CharSequence? = null
- var affiliatedDomain: CharSequence? = null
-
slice.items.forEach {
- if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+ if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+ beginGetCredentialOptionId = it.text
+ } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+ entryGroupId = it.text
+ } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+ val defaultIconValue = it.text
+ if (defaultIconValue == TRUE_STRING) {
+ isDefaultIconPreferredAsSingleProvider = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+ affiliatedDomain = it.text
+ } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
typeDisplayName = it.text
} else if (it.hasHint(SLICE_HINT_TITLE)) {
title = it.text
@@ -372,8 +525,6 @@
icon = it.icon
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
- } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
- beginGetPublicKeyCredentialOptionId = it.text
} else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
lastUsedTime = Instant.ofEpochMilli(it.long)
} else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -383,20 +534,10 @@
}
} else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
autoSelectAllowedFromOption = true
- } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
- val defaultIconValue = it.text
- if (defaultIconValue == TRUE_STRING) {
- isDefaultIconPreferredAsSingleProvider = true
- }
} else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
isDefaultIcon = true
- } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
- entryGroupId = it.text
- } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
- affiliatedDomain = it.text
}
}
-
return try {
PublicKeyCredentialEntry(
username = title!!,
@@ -409,7 +550,7 @@
beginGetPublicKeyCredentialOption =
BeginGetPublicKeyCredentialOption.createFromEntrySlice(
Bundle(),
- beginGetPublicKeyCredentialOptionId!!.toString(),
+ beginGetCredentialOptionId!!.toString(),
),
entryGroupId = entryGroupId,
isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
@@ -428,51 +569,6 @@
companion object {
private const val TAG = "PublicKeyCredEntry"
- private const val SLICE_HINT_TYPE_DISPLAY_NAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_TITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
- private const val SLICE_HINT_SUBTITLE =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
- private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
- private const val SLICE_HINT_ICON =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
- private const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
- private const val SLICE_HINT_AUTO_ALLOWED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
- private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
- private const val SLICE_HINT_OPTION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
- private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
- private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
- private const val SLICE_HINT_AFFILIATED_DOMAIN =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
- private const val SLICE_HINT_DEDUPLICATION_ID =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
- private const val TRUE_STRING = "true"
-
- private const val FALSE_STRING = "false"
-
- private const val REVISION_ID = 1
-
/**
* Converts an instance of [PublicKeyCredentialEntry] to a [Slice].
*
@@ -482,14 +578,16 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
fun toSlice(entry: PublicKeyCredentialEntry): Slice? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.toSlice(entry)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.toSlice(entry)
}
return null
}
/**
- * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ * Returns an instance of [PublicKeyCredentialEntry] derived from a [Slice] object.
*
* @param slice the [Slice] object constructed through [toSlice]
*/
@@ -497,7 +595,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.fromSlice(slice)
+ } else if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.fromSlice(slice)
}
return null
@@ -536,6 +636,7 @@
private var icon: Icon? = null
private var autoSelectAllowed: Boolean = false
private var isDefaultIconPreferredAsSingleProvider: Boolean = false
+ private var biometricPromptData: BiometricPromptData? = null
/** Sets a displayName to be shown on the UI with this entry */
fun setDisplayName(displayName: CharSequence?): Builder {
@@ -549,6 +650,18 @@
return this
}
+ /**
+ * Sets the biometric prompt data to optionally utilize a credential manager flow that
+ * directly handles the biometric verification for you and gives you the response; set to
+ * null by default, indicating the default behavior is to not utilize this embedded
+ * biometric prompt flow.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+ this.biometricPromptData = biometricPromptData
+ return this
+ }
+
/** Sets whether the entry should be auto-selected. The value is false by default */
@Suppress("MissingGetterMatchingBuilder")
fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -585,15 +698,16 @@
val typeDisplayName =
context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL)
return PublicKeyCredentialEntry(
- username,
- displayName,
- typeDisplayName,
- pendingIntent,
- icon!!,
- lastUsedTime,
- autoSelectAllowed,
- beginGetPublicKeyCredentialOption,
- isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
+ username = username,
+ displayName = displayName,
+ typeDisplayName = typeDisplayName,
+ pendingIntent = pendingIntent,
+ icon = icon!!,
+ lastUsedTime = lastUsedTime,
+ isAutoSelectAllowed = autoSelectAllowed,
+ beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+ biometricPromptData = biometricPromptData,
)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
index 1ee7c00..d542e03 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:Suppress("deprecation") // For usage of Slice
+
package androidx.credentials.provider
import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt
new file mode 100644
index 0000000..53417f2
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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.credentials.provider.utils
+
+import android.os.Build
+
+internal const val ANDROID_15_BETA_3 = "AP31.240517.022"
+internal const val ANDROID_15_BETA_2_2 = "AP31.240426.023.B4"
+internal const val ANDROID_15_BETA_2_1 = "AP31.240426.023"
+internal const val ANDROID_15_BETA_2 = "AP31.240426.022"
+
+internal val buildsUsingSliceProperties =
+ setOf(ANDROID_15_BETA_2, ANDROID_15_BETA_2_1, ANDROID_15_BETA_2_2, ANDROID_15_BETA_3)
+
+/**
+ * The library owners aim to support early partners across beta2 and beta3 devices for Android 15,
+ * requiring this to be introduced for backwards compatibility. Beyond this temporary use case, the
+ * library owners aim to no longer utilize this functionality.
+ */
+internal fun requiresSlicePropertiesWorkaround(): Boolean =
+ buildsUsingSliceProperties.contains(Build.ID)
diff --git a/credentials/credentials/src/main/res/values-hy/strings.xml b/credentials/credentials/src/main/res/values-hy/strings.xml
index 617300a0..2dfb774f 100644
--- a/credentials/credentials/src/main/res/values-hy/strings.xml
+++ b/credentials/credentials/src/main/res/values-hy/strings.xml
@@ -17,6 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Անցաբառ"</string>
+ <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Մուտքի բանալի"</string>
<string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Գաղտնաբառ"</string>
</resources>
diff --git a/credentials/credentials/src/main/res/values-nb/strings.xml b/credentials/credentials/src/main/res/values-nb/strings.xml
index a72318a..9eb70fe 100644
--- a/credentials/credentials/src/main/res/values-nb/strings.xml
+++ b/credentials/credentials/src/main/res/values-nb/strings.xml
@@ -17,6 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Tilgangsnøkkel"</string>
+ <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passnøkkel"</string>
<string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passord"</string>
</resources>
diff --git a/credentials/credentials/src/main/res/values-tr/strings.xml b/credentials/credentials/src/main/res/values-tr/strings.xml
index f00b298..02256b8 100644
--- a/credentials/credentials/src/main/res/values-tr/strings.xml
+++ b/credentials/credentials/src/main/res/values-tr/strings.xml
@@ -17,6 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Şifre anahtarı"</string>
+ <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Geçiş anahtarı"</string>
<string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Şifre"</string>
</resources>
diff --git a/credentials/credentials/src/main/res/values-uz/strings.xml b/credentials/credentials/src/main/res/values-uz/strings.xml
index 7f1bb8c..77100e0 100644
--- a/credentials/credentials/src/main/res/values-uz/strings.xml
+++ b/credentials/credentials/src/main/res/values-uz/strings.xml
@@ -17,6 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kod"</string>
+ <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kirish kaliti"</string>
<string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
</resources>
diff --git a/credentials/credentials/src/main/res/values/ids.xml b/credentials/credentials/src/main/res/values/ids.xml
new file mode 100644
index 0000000..3aac762
--- /dev/null
+++ b/credentials/credentials/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="androidx_credential_pendingCredentialRequest" type="id"/>
+</resources>
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 2e5f736..07d5fe6 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -54,6 +54,7 @@
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
}
android {
+ compileSdk 35
namespace 'com.example.datastorecomposesamples'
defaultConfig {
minSdk 28
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index 6f9146c..3cabbbf 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.
@@ -194,4 +194,4 @@
#-----------------------------------------------------------------------
# Enable compose @Preview rendering
#-----------------------------------------------------------------------
-compose.project.uses.compose.override=true
+compose.project.uses.compose.override=true
\ No newline at end of file
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 897a4ac..bb7a6a4 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -21,6 +21,7 @@
samples("androidx.window:window-samples:1.3.0-alpha03")
docsForOptionalProject(":xr:xr")
+ docsForOptionalProject(":xr:xr-material3-adaptive")
docs(project(":activity:activity"))
docs(project(":activity:activity-compose"))
docs(project(":activity:activity-ktx"))
diff --git a/emoji2/emoji2-emojipicker/samples/build.gradle b/emoji2/emoji2-emojipicker/samples/build.gradle
index 6824517..3af2482 100644
--- a/emoji2/emoji2-emojipicker/samples/build.gradle
+++ b/emoji2/emoji2-emojipicker/samples/build.gradle
@@ -34,6 +34,7 @@
implementation(project(":compose:foundation:foundation"))
}
android {
+ compileSdk 35
namespace "androidx.emoji2.emojipicker.samples"
}
diff --git a/gradle.properties b/gradle.properties
index 6edafda..6cd68fb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,19 +32,19 @@
# Remove when AGP defaults to 2.1.0
android.prefabVersion=2.1.0
-# Do generate versioned API files
-androidx.writeVersionedApiFiles=true
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
-# Do run the CheckAarMetadata task
-android.experimental.disableCompileSdkChecks=false
+# Don't run the CheckAarMetadata task
+android.experimental.disableCompileSdkChecks=true
# Don't warn about needing to update AGP
-android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33,34
+android.suppressUnsupportedCompileSdk=35
androidx.compileSdk=34
androidx.targetSdkVersion=34
androidx.allowCustomCompileSdk=true
-androidx.includeOptionalProjects=false
+androidx.includeOptionalProjects=true
# Disable features we do not use
android.defaults.buildfeatures.aidl=false
diff --git a/hilt/integration-tests/viewmodelapp/build.gradle b/hilt/integration-tests/viewmodelapp/build.gradle
index 0db4203..12eec0a 100644
--- a/hilt/integration-tests/viewmodelapp/build.gradle
+++ b/hilt/integration-tests/viewmodelapp/build.gradle
@@ -23,6 +23,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
testInstrumentationRunner "androidx.hilt.integration.viewmodelapp.TestRunner"
}
diff --git a/hilt/integration-tests/workerapp/build.gradle b/hilt/integration-tests/workerapp/build.gradle
index 391f756..b277151 100644
--- a/hilt/integration-tests/workerapp/build.gradle
+++ b/hilt/integration-tests/workerapp/build.gradle
@@ -23,6 +23,7 @@
}
android {
+ compileSdk 35
defaultConfig {
testInstrumentationRunner "androidx.hilt.integration.workerapp.TestRunner"
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 8083aae..9a6f0d9 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -71,7 +71,8 @@
}
android {
- namespace = "androidx.ink.brush"
+ compileSdk 35
+ namespace = "androidx.ink.brush"
}
androidx {
diff --git a/libraryversions.toml b/libraryversions.toml
index a6f57a1..fd09be2 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -8,7 +8,7 @@
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
AUTOFILL = "1.3.0-alpha02"
BENCHMARK = "1.3.0-rc01"
-BIOMETRIC = "1.2.0-alpha06"
+BIOMETRIC = "1.4.0-alpha01"
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
@@ -30,7 +30,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.14.0-alpha01"
+CORE = "1.15.0-alpha01"
CORE_ANIMATION = "1.0.0"
CORE_ANIMATION_TESTING = "1.0.0"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -44,7 +44,7 @@
CORE_SPLASHSCREEN = "1.2.0-alpha01"
CORE_TELECOM = "1.0.0-alpha10"
CORE_UWB = "1.0.0-alpha08"
-CREDENTIALS = "1.3.0-rc01"
+CREDENTIALS = "1.5.0-alpha03"
CREDENTIALS_E2EE_QUARANTINE = "1.0.0-alpha02"
CREDENTIALS_FIDO_QUARANTINE = "1.0.0-alpha02"
CURSORADAPTER = "1.1.0-alpha01"
@@ -116,7 +116,7 @@
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
REMOTECALLBACK = "1.0.0-alpha02"
RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.7.0-alpha06"
+ROOM = "2.7.0-alpha05"
SAFEPARCEL = "1.0.0-alpha01"
SAVEDSTATE = "1.3.0-alpha01"
SECURITY = "1.1.0-alpha07"
@@ -132,7 +132,7 @@
SLICE_BUILDERS_KTX = "1.0.0-alpha09"
SLICE_REMOTECALLBACK = "1.0.0-alpha01"
SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.5.0-alpha06"
+SQLITE = "2.5.0-alpha05"
SQLITE_INSPECTOR = "2.1.0-alpha01"
STABLE_AIDL = "1.0.0-alpha01"
STARTUP = "1.2.0-alpha03"
@@ -169,8 +169,8 @@
WEAR_WATCHFACE = "1.3.0-alpha03"
WEBKIT = "1.12.0-alpha02"
# Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.4.0-alpha01"
-WINDOW_EXTENSIONS = "1.4.0-alpha01"
+WINDOW = "1.6.0-alpha01"
+WINDOW_EXTENSIONS = "1.6.0-alpha01"
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
WORK = "2.10.0-alpha02"
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 703f61c..80ec967 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -80,6 +80,8 @@
}
android {
+ compileSdk 35
+
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 05e15d6..db0b559 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -99,5 +99,6 @@
}
android {
+ compileSdk 35
namespace "androidx.lifecycle.viewmodel.compose"
}
diff --git a/loader/loader-ktx/build.gradle b/loader/loader-ktx/build.gradle
index c700ba7..cf8f944f 100644
--- a/loader/loader-ktx/build.gradle
+++ b/loader/loader-ktx/build.gradle
@@ -56,5 +56,6 @@
}
android {
+ compileSdk 35
namespace "androidx.loader.ktx"
}
diff --git a/loader/loader/build.gradle b/loader/loader/build.gradle
index 5e08078..cf1b2a0 100644
--- a/loader/loader/build.gradle
+++ b/loader/loader/build.gradle
@@ -38,5 +38,6 @@
}
android {
+ compileSdk 35
namespace "androidx.loader"
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
index ec6a61b..1ab6667 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
@@ -596,4 +596,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index a12969f..e88cc01 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -162,6 +162,7 @@
ApplicationInfo appInfo = activityInfo.applicationInfo;
if (((ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
& appInfo.flags) != 0) {
+ intent.setPackage(appInfo.packageName);
context.startActivity(intent);
return true;
}
@@ -191,6 +192,7 @@
ApplicationInfo appInfo = activityInfo.applicationInfo;
if (((ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
& appInfo.flags) != 0) {
+ intent.setPackage(appInfo.packageName);
context.startActivity(intent);
return true;
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index f4a25ae..201dd205 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -404,6 +404,13 @@
/* package */ void selectRoute(
@NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
+ selectRoute(route, unselectReason, /* syncMediaRoute1Provider= */ true);
+ }
+
+ /* package */ void selectRoute(
+ @NonNull MediaRouter.RouteInfo route,
+ @MediaRouter.UnselectReason int unselectReason,
+ boolean syncMediaRoute1Provider) {
if (!mRoutes.contains(route)) {
Log.w(TAG, "Ignoring attempt to select removed route: " + route);
return;
@@ -420,7 +427,7 @@
&& mSelectedRoute != route) {
mMr2Provider.transferTo(route.getDescriptorId());
} else {
- selectRouteInternal(route, unselectReason);
+ selectRouteInternal(route, unselectReason, syncMediaRoute1Provider);
}
}
@@ -922,7 +929,15 @@
"Unselecting the current route because it "
+ "is no longer selectable: "
+ mSelectedRoute);
- selectRouteInternal(chooseFallbackRoute(), UNSELECT_REASON_UNKNOWN);
+ // TODO: b/294968421 - Consider passing a false syncMediaRoute1Provider. This could help
+ // with the prevention of setBluetoothA2dpOn(false) bugs, but it could also leave the
+ // platform MediaRouter in an inconsistent state. In order to change
+ // syncMediaRoute1Provider to false, we need to assess the impact of not calling
+ // android.media.MediaRouter.selectRoute as a result of this method call.
+ selectRouteInternal(
+ chooseFallbackRoute(),
+ UNSELECT_REASON_UNKNOWN,
+ /* syncMediaRoute1Provider= */ true);
} else if (selectedRouteDescriptorChanged) {
// In case the selected route is a route group, select/unselect route controllers
// for the added/removed route members.
@@ -958,7 +973,9 @@
}
/* package */ void selectRouteInternal(
- @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
+ @NonNull MediaRouter.RouteInfo route,
+ @MediaRouter.UnselectReason int unselectReason,
+ boolean syncMediaRoute1Provider) {
if (mSelectedRoute == route) {
return;
}
@@ -1049,16 +1066,18 @@
if (mSelectedRoute == null) {
mSelectedRoute = route;
mSelectedRouteController = routeController;
- mCallbackHandler.post(
- GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
- new Pair<>(null, route),
- unselectReason);
+ mCallbackHandler.postRouteSelectedMessage(
+ /* fromRoute= */ null,
+ /* targetRoute= */ route,
+ unselectReason,
+ syncMediaRoute1Provider);
} else {
notifyTransfer(
this,
route,
routeController,
unselectReason,
+ syncMediaRoute1Provider,
/* requestedRoute= */ null,
/* memberRoutes= */ null);
}
@@ -1106,19 +1125,26 @@
MediaRouter.RouteInfo route,
@Nullable MediaRouteProvider.RouteController routeController,
@MediaRouter.UnselectReason int reason,
+ boolean syncMediaRoute1Provider,
@Nullable MediaRouter.RouteInfo requestedRoute,
@Nullable
- Collection<
- MediaRouteProvider.DynamicGroupRouteController
- .DynamicRouteDescriptor>
- memberRoutes) {
+ Collection<
+ MediaRouteProvider.DynamicGroupRouteController
+ .DynamicRouteDescriptor>
+ memberRoutes) {
if (mTransferNotifier != null) {
mTransferNotifier.cancel();
mTransferNotifier = null;
}
mTransferNotifier =
new MediaRouter.PrepareTransferNotifier(
- router, route, routeController, reason, requestedRoute, memberRoutes);
+ router,
+ route,
+ routeController,
+ reason,
+ syncMediaRoute1Provider,
+ requestedRoute,
+ memberRoutes);
if (mTransferNotifier.mReason != UNSELECT_REASON_ROUTE_CHANGED
|| mOnPrepareTransferListener == null) {
@@ -1137,51 +1163,52 @@
/* package */ MediaRouteProvider.DynamicGroupRouteController.OnDynamicRoutesChangedListener
mDynamicRoutesListener =
- new MediaRouteProvider.DynamicGroupRouteController
- .OnDynamicRoutesChangedListener() {
- @Override
- public void onRoutesChanged(
- @NonNull MediaRouteProvider.DynamicGroupRouteController controller,
- @Nullable MediaRouteDescriptor groupRouteDescriptor,
- @NonNull
- Collection<
- MediaRouteProvider
- .DynamicGroupRouteController
- .DynamicRouteDescriptor>
- routes) {
- if (controller == mRequestedRouteController
- && groupRouteDescriptor != null) {
- MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
- String groupId = groupRouteDescriptor.getId();
+ new MediaRouteProvider.DynamicGroupRouteController
+ .OnDynamicRoutesChangedListener() {
+ @Override
+ public void onRoutesChanged(
+ @NonNull MediaRouteProvider.DynamicGroupRouteController controller,
+ @Nullable MediaRouteDescriptor groupRouteDescriptor,
+ @NonNull
+ Collection<
+ MediaRouteProvider
+ .DynamicGroupRouteController
+ .DynamicRouteDescriptor>
+ routes) {
+ if (controller == mRequestedRouteController
+ && groupRouteDescriptor != null) {
+ MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
+ String groupId = groupRouteDescriptor.getId();
- String uniqueId = assignRouteUniqueId(provider, groupId);
- MediaRouter.RouteInfo route =
- new MediaRouter.RouteInfo(provider, groupId, uniqueId);
- route.maybeUpdateDescriptor(groupRouteDescriptor);
+ String uniqueId = assignRouteUniqueId(provider, groupId);
+ MediaRouter.RouteInfo route =
+ new MediaRouter.RouteInfo(provider, groupId, uniqueId);
+ route.maybeUpdateDescriptor(groupRouteDescriptor);
- if (mSelectedRoute == route) {
- return;
+ if (mSelectedRoute == route) {
+ return;
+ }
+
+ notifyTransfer(
+ GlobalMediaRouter.this,
+ route,
+ mRequestedRouteController,
+ UNSELECT_REASON_ROUTE_CHANGED,
+ /* syncMediaRoute1Provider= */ true,
+ mRequestedRoute,
+ routes);
+
+ mRequestedRoute = null;
+ mRequestedRouteController = null;
+ } else if (controller == mSelectedRouteController) {
+ if (groupRouteDescriptor != null) {
+ updateRouteDescriptorAndNotify(
+ mSelectedRoute, groupRouteDescriptor);
+ }
+ mSelectedRoute.updateDynamicDescriptors(routes);
+ }
}
-
- notifyTransfer(
- GlobalMediaRouter.this,
- route,
- mRequestedRouteController,
- UNSELECT_REASON_ROUTE_CHANGED,
- mRequestedRoute,
- routes);
-
- mRequestedRoute = null;
- mRequestedRouteController = null;
- } else if (controller == mSelectedRouteController) {
- if (groupRouteDescriptor != null) {
- updateRouteDescriptorAndNotify(
- mSelectedRoute, groupRouteDescriptor);
- }
- mSelectedRoute.updateDynamicDescriptors(routes);
- }
- }
- };
+ };
@Override
public void onPlatformRouteSelectedByDescriptorId(@NonNull String id) {
@@ -1329,7 +1356,12 @@
return;
}
- selectRouteInternal(routeToSelect, reason);
+ // TODO: b/294968421 - Consider passing a false syncMediaRoute1Provider. This could help
+ // with the prevention of setBluetoothA2dpOn(false) bugs, but it could also leave the
+ // platform MediaRouter in an inconsistent state. In order to change
+ // syncMediaRoute1Provider to false, we need to assess the impact of not calling
+ // android.media.MediaRouter.selectRoute as a result of this method call.
+ selectRouteInternal(routeToSelect, reason, /* syncMediaRoute1Provider */ true);
}
@Override
@@ -1356,7 +1388,7 @@
/* package */ void selectRouteToFallbackRoute(@MediaRouter.UnselectReason int reason) {
MediaRouter.RouteInfo fallbackRoute = chooseFallbackRoute();
if (getSelectedRoute() != fallbackRoute) {
- selectRouteInternal(fallbackRoute, reason);
+ selectRouteInternal(fallbackRoute, reason, /* syncMediaRoute1Provider */ true);
}
// Does nothing when the selected route is same with fallback route.
// This is the difference between this and unselect().
@@ -1497,7 +1529,29 @@
public static final int MSG_ROUTER_PARAMS_CHANGED = MSG_TYPE_ROUTER | 1;
- CallbackHandler() {
+ /* package */ void postRouteSelectedMessage(
+ @Nullable MediaRouter.RouteInfo fromRoute,
+ @NonNull MediaRouter.RouteInfo targetRoute,
+ int reason,
+ boolean syncMediaRoute1Provider) {
+ RouteSelectedMessageParams params =
+ new RouteSelectedMessageParams(fromRoute, targetRoute, syncMediaRoute1Provider);
+ Message message = obtainMessage(MSG_ROUTE_SELECTED, params);
+ message.arg1 = reason;
+ message.sendToTarget();
+ }
+
+ /* package */ void postAnotherRouteSelectedMessage(
+ @Nullable MediaRouter.RouteInfo requestedRoute,
+ @NonNull MediaRouter.RouteInfo targetRoute,
+ int reason,
+ boolean syncMediaRoute1Provider) {
+ RouteSelectedMessageParams params =
+ new RouteSelectedMessageParams(
+ requestedRoute, targetRoute, syncMediaRoute1Provider);
+ Message message = obtainMessage(MSG_ROUTE_ANOTHER_SELECTED, params);
+ message.arg1 = reason;
+ message.sendToTarget();
}
/* package */ void post(int msg, Object obj) {
@@ -1562,9 +1616,11 @@
(MediaRouter.RouteInfo) obj);
break;
case MSG_ROUTE_SELECTED: {
- MediaRouter.RouteInfo selectedRoute =
- ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
- mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(selectedRoute);
+ RouteSelectedMessageParams params = (RouteSelectedMessageParams) obj;
+ MediaRouter.RouteInfo selectedRoute = params.mTargetRoute;
+ if (params.mSyncMediaRoute1Provider) {
+ mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(selectedRoute);
+ }
// TODO(b/166794092): Remove this nullness check
if (mDefaultRoute != null && selectedRoute.isDefaultOrBluetooth()) {
for (MediaRouter.RouteInfo prevGroupRoute : mDynamicGroupRoutes) {
@@ -1575,11 +1631,13 @@
break;
}
case MSG_ROUTE_ANOTHER_SELECTED: {
- MediaRouter.RouteInfo groupRoute =
- ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
+ RouteSelectedMessageParams params = (RouteSelectedMessageParams) obj;
+ MediaRouter.RouteInfo groupRoute = params.mTargetRoute;
mDynamicGroupRoutes.add(groupRoute);
mPlatformMediaRouter1RouteProvider.onSyncRouteAdded(groupRoute);
- mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(groupRoute);
+ if (params.mSyncMediaRoute1Provider) {
+ mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(groupRoute);
+ }
break;
}
}
@@ -1592,16 +1650,18 @@
final MediaRouter.Callback callback = record.mCallback;
switch (what & MSG_TYPE_MASK) {
case MSG_TYPE_ROUTE: {
+ RouteSelectedMessageParams selectedMessageParams =
+ what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED
+ ? ((RouteSelectedMessageParams) obj)
+ : null;
final MediaRouter.RouteInfo route =
- (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
- ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
- .second
- : (MediaRouter.RouteInfo) obj;
+ selectedMessageParams != null
+ ? selectedMessageParams.mTargetRoute
+ : (MediaRouter.RouteInfo) obj;
final MediaRouter.RouteInfo optionalRoute =
- (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
- ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
- .first
- : null;
+ selectedMessageParams != null
+ ? selectedMessageParams.mFromOrRequestedRoute
+ : null;
if (route == null
|| !record.filterRouteEvent(route, what, optionalRoute, arg)) {
break;
@@ -1661,4 +1721,29 @@
}
}
}
+
+ /**
+ * Holds the parameters of {@link CallbackHandler#MSG_ROUTE_SELECTED} and {@link
+ * CallbackHandler#MSG_ROUTE_ANOTHER_SELECTED}.
+ */
+ private static final class RouteSelectedMessageParams {
+ /**
+ * Holds the origin route for {@link CallbackHandler#MSG_ROUTE_SELECTED}, or the originally
+ * requested route for {@link CallbackHandler#MSG_ROUTE_ANOTHER_SELECTED}.
+ */
+ @Nullable public final MediaRouter.RouteInfo mFromOrRequestedRoute;
+
+ @NonNull public final MediaRouter.RouteInfo mTargetRoute;
+
+ public final boolean mSyncMediaRoute1Provider;
+
+ private RouteSelectedMessageParams(
+ @Nullable MediaRouter.RouteInfo fromOrRequestedRoute,
+ @NonNull MediaRouter.RouteInfo targetRoute,
+ boolean syncMediaRoute1Provider) {
+ mFromOrRequestedRoute = fromOrRequestedRoute;
+ mTargetRoute = targetRoute;
+ mSyncMediaRoute1Provider = syncMediaRoute1Provider;
+ }
+ }
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index 462b25e..6220936 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -405,7 +405,6 @@
}
private class TransferCallback extends MediaRouter2.TransferCallback {
- TransferCallback() {}
@Override
public void onTransfer(@NonNull MediaRouter2.RoutingController oldController,
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 1a6bad4..faabbdc 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -43,7 +43,6 @@
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.core.util.ObjectsCompat;
-import androidx.core.util.Pair;
import androidx.mediarouter.app.MediaRouteDiscoveryFragment;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
@@ -1979,11 +1978,29 @@
*/
@MainThread
public void select() {
- checkCallingThread();
- getGlobalRouter().selectRoute(this, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
+ select(/* syncMediaRoute1Provider= */ true);
}
/**
+ * Selects this media route.
+ *
+ * @param syncMediaRoute1Provider Whether this selection should be passed through to {@link
+ * PlatformMediaRouter1RouteProvider}. Should be false when this call is the result of a
+ * {@link MediaRouter.Callback#onRouteSelected} call.
+ */
+ @RestrictTo(LIBRARY)
+ @MainThread
+ public void select(boolean syncMediaRoute1Provider) {
+ checkCallingThread();
+ getGlobalRouter()
+ .selectRoute(
+ this,
+ MediaRouter.UNSELECT_REASON_ROUTE_CHANGED,
+ syncMediaRoute1Provider);
+ }
+
+
+ /**
* Returns true if the route has one or more members
*/
@RestrictTo(LIBRARY)
@@ -2691,6 +2708,7 @@
final RouteController mToRouteController;
final @UnselectReason int mReason;
+ private final boolean mSyncMediaRoute1Provider;
private final RouteInfo mFromRoute;
final RouteInfo mToRoute;
private final RouteInfo mRequestedRoute;
@@ -2702,8 +2720,12 @@
private boolean mFinished = false;
private boolean mCanceled = false;
- PrepareTransferNotifier(GlobalMediaRouter router, RouteInfo route,
- @Nullable RouteController routeController, @UnselectReason int reason,
+ PrepareTransferNotifier(
+ GlobalMediaRouter router,
+ RouteInfo route,
+ @Nullable RouteController routeController,
+ @UnselectReason int reason,
+ boolean syncMediaRoute1Provider,
@Nullable RouteInfo requestedRoute,
@Nullable Collection<DynamicRouteDescriptor> memberRoutes) {
mRouter = new WeakReference<>(router);
@@ -2711,6 +2733,7 @@
mToRoute = route;
mToRouteController = routeController;
mReason = reason;
+ mSyncMediaRoute1Provider = syncMediaRoute1Provider;
mFromRoute = router.mSelectedRoute;
mRequestedRoute = requestedRoute;
mMemberRoutes = (memberRoutes == null) ? null : new ArrayList<>(memberRoutes);
@@ -2807,12 +2830,11 @@
router.mSelectedRouteController = mToRouteController;
if (mRequestedRoute == null) {
- router.mCallbackHandler.post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
- new Pair<>(mFromRoute, mToRoute), mReason);
+ router.mCallbackHandler.postRouteSelectedMessage(
+ mFromRoute, mToRoute, mReason, mSyncMediaRoute1Provider);
} else {
- router.mCallbackHandler.post(
- GlobalMediaRouter.CallbackHandler.MSG_ROUTE_ANOTHER_SELECTED,
- new Pair<>(mRequestedRoute, mToRoute), mReason);
+ router.mCallbackHandler.postAnotherRouteSelectedMessage(
+ mRequestedRoute, mToRoute, mReason, mSyncMediaRoute1Provider);
}
router.mRouteControllerMap.clear();
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
index 52c2cce..322240f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
@@ -312,7 +312,7 @@
UserRouteRecord userRouteRecord = getUserRouteRecord(route);
if (userRouteRecord != null) {
- userRouteRecord.mRoute.select();
+ userRouteRecord.mRoute.select(/* syncMediaRoute1Provider= */ false);
} else {
// Select the route if it already exists in the compat media router.
// If not, we will select it instead when the route is added.
diff --git a/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml b/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
index 4d5ffc5..c75c58e 100644
--- a/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
@@ -51,8 +51,12 @@
<string name="mr_chooser_wifi_warning_description_unknown" msgid="3459891599800041449">"确保另一设备与此设备连接到同一 WLAN 网络"</string>
<string name="mr_chooser_wifi_learn_more" msgid="3799500840179081429"><a href="https://support.google.com/chromecast/?p=trouble-finding-devices">"了解详情"</a></string>
<string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"了解如何投放"</string>
- <string name="mr_route_name_unknown" msgid="5538521943939635302">"未知"</string>
- <string name="mr_route_name_bluetooth" msgid="5056346328175584455">"蓝牙"</string>
- <string name="mr_route_name_tv" msgid="8041420425123528188">"电视"</string>
- <string name="mr_route_name_speaker" msgid="708574147123374685">"音箱"</string>
+ <!-- no translation found for mr_route_name_unknown (5538521943939635302) -->
+ <skip />
+ <!-- no translation found for mr_route_name_bluetooth (5056346328175584455) -->
+ <skip />
+ <!-- no translation found for mr_route_name_tv (8041420425123528188) -->
+ <skip />
+ <!-- no translation found for mr_route_name_speaker (708574147123374685) -->
+ <skip />
</resources>
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index da13b35..90a1980 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -24,6 +24,7 @@
buildFeatures {
viewBinding true
}
+ compileSdk 35
namespace "androidx.metrics.performance.janktest"
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 03a6aca..d532d8a 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -50,5 +50,6 @@
}
android {
+ compileSdk 35
namespace "androidx.navigation.compose.demos"
}
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b7b3ffb..19e494b 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -59,5 +59,6 @@
}
android {
+ compileSdk 35
namespace "androidx.navigation.compose.samples"
}
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index 0988655..cbd8d34 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -55,6 +55,7 @@
}
android {
+ compileSdk 35
namespace "androidx.paging.integration.testapp"
}
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 020a4f2..1a9503f 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -86,6 +86,7 @@
}
android {
+ compileSdk 35
namespace "androidx.paging.compose"
// TODO(b/313699418): need to update compose.runtime version to 1.6.0+
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index e9cb7bb..2541828 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -56,5 +56,6 @@
}
android {
+ compileSdk 35
namespace "androidx.paging.compose.demos"
}
diff --git a/paging/paging-compose/samples/build.gradle b/paging/paging-compose/samples/build.gradle
index 1df0616..2980ac1 100644
--- a/paging/paging-compose/samples/build.gradle
+++ b/paging/paging-compose/samples/build.gradle
@@ -48,5 +48,6 @@
}
android {
+ compileSdk 35
namespace "androidx.paging.compose.samples"
}
diff --git a/pdf/integration-tests/testapp/build.gradle b/pdf/integration-tests/testapp/build.gradle
index 1b1a2e5..021fa34 100644
--- a/pdf/integration-tests/testapp/build.gradle
+++ b/pdf/integration-tests/testapp/build.gradle
@@ -6,17 +6,17 @@
android {
namespace 'androidx.pdf.testapp'
+ buildToolsVersion "35.0.0-rc1"
defaultConfig {
applicationId "androidx.pdf.testapp"
- minSdkVersion 31
+ minSdk 31
+ compileSdk 35
}
}
dependencies {
-
api("com.google.android.material:material:1.11.0")
implementation(project(":pdf:pdf-viewer"))
-
implementation(libs.constraintLayout)
}
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
index 4a33224..6caf23a 100644
--- a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -27,10 +27,9 @@
android:theme="@style/Theme.Androidx"
tools:replace="android:label">
<activity
- android:name=".MainActivity"
+ android:name=".LegacyMainActivity"
android:exported="true"
- android:windowSoftInputMode="adjustResize"
- android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
+ android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java b/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java
new file mode 100644
index 0000000..01344f4
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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.pdf.testapp;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.pdf.viewer.PdfViewer;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings({"deprecation", "RestrictedApiAndroidX"})
+public class LegacyMainActivity extends AppCompatActivity {
+
+ private PdfViewer mPdfViewer;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Button getContentButton = findViewById(R.id.launch_button);
+ assert getContentButton != null;
+ getContentButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("application/pdf");
+ startActivityForResult(intent, 2);
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestcode, int resultcode, @Nullable Intent data) {
+ super.onActivityResult(requestcode, resultcode, data);
+ assert data != null;
+ Uri uri = data.getData();
+ if (uri != null) {
+ setPdfViewer();
+ mPdfViewer.loadFile(uri);
+ }
+ }
+
+ void setPdfViewer() {
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+
+ mPdfViewer = new PdfViewer();
+ transaction.replace(R.id.fragment_container_view, mPdfViewer, null);
+ transaction.commitAllowingStateLoss();
+ fragmentManager.executePendingTransactions();
+ }
+}
diff --git a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 7820e52..a8a25f6 100644
--- a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -23,7 +23,7 @@
android:layout_gravity="center"
android:fitsSystemWindows="true"
android:orientation="vertical"
- tools:context=".MainActivity">
+ tools:context=".LegacyMainActivity">
<com.google.android.material.button.MaterialButton
android:id="@+id/launch_button"
diff --git a/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml b/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml
new file mode 100644
index 0000000..5b89e97
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 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.
+ -->
+
+<resources>
+ <string name="launch_string">Launch AOSP PDF Viewer</string>
+</resources>
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml b/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..df40dfd
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml
@@ -0,0 +1,23 @@
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Base.Theme.Androidx" parent="Theme.Material3.DayNight.NoActionBar">
+ <!-- Customize your dark theme here. -->
+ <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
+ </style>
+</resources>
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/res/values/colors.xml b/pdf/integration-tests/testapp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..8175d9c
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 787fa02..75de71e 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -30,6 +30,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.exifinterface:exifinterface:1.3.2")
+ implementation(project(":core:core"))
testImplementation(libs.junit)
testImplementation(libs.testCore)
@@ -59,7 +60,9 @@
namespace "androidx.pdf"
defaultConfig {
- minSdkVersion 30
+ minSdk 31
+ buildToolsVersion "35.0.0-rc1"
+ compileSdk 35
}
buildFeatures {
@@ -72,12 +75,6 @@
}
}
- externalNativeBuild {
- cmake {
- path file('src/main/native/CMakeLists.txt')
- version libs.versions.cmake.get()
- }
- }
sourceSets {
test {
assets {
diff --git a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
index 1466502..fe945ea 100644
--- a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
+++ b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
@@ -25,7 +25,7 @@
import android.os.RemoteException;
import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
diff --git a/pdf/pdf-viewer/src/main/AndroidManifest.xml b/pdf/pdf-viewer/src/main/AndroidManifest.xml
index cd24525..eadf1dd 100644
--- a/pdf/pdf-viewer/src/main/AndroidManifest.xml
+++ b/pdf/pdf-viewer/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@
<application android:label="PdfViewer">
<service
- android:name="androidx.pdf.pdflib.PdfDocumentService"
+ android:name="androidx.pdf.service.PdfDocumentService"
android:isolatedProcess="true"
tools:ignore="MissingServiceExportedEqualsTrue" />
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
index b029958..44ac3b1 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
@@ -18,14 +18,19 @@
import android.annotation.SuppressLint;
import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import com.google.common.base.Preconditions;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -77,6 +82,25 @@
}
/**
+ * Converts android.graphics.pdf.content.PdfPageGotoLinkContent object to its
+ * androidx.pdf.aidl.GotoLink representation.
+ */
+ @NonNull
+ public static GotoLink convert(@NonNull PdfPageGotoLinkContent pdfPageGotoLinkContent) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ List<Rect> rectBounds = new ArrayList<>();
+ List<RectF> rectFBounds = pdfPageGotoLinkContent.getBounds();
+ for (RectF rectF : rectFBounds) {
+ rectBounds.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+ (int) rectF.bottom));
+ }
+ return new GotoLink(rectBounds,
+ GotoLinkDestination.convert(pdfPageGotoLinkContent.getDestination()));
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ /**
* Gets the bounds of a {@link GotoLink} represented as a list of {@link Rect}.
* Links which are spread across multiple lines will be surrounded by multiple {@link Rect}
* in order of viewing.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
index 11c2137..e7eae7f 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
@@ -17,8 +17,11 @@
package androidx.pdf.models;
import android.annotation.SuppressLint;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -140,4 +143,20 @@
parcel.writeFloat(mYCoordinate);
parcel.writeFloat(mZoom);
}
+
+ /**
+ * Converts android.graphics.pdf.content.PdfPageGotoLinkContent.Destination object to its
+ * androidx.pdf.aidl.GotoLinkDestination representation.
+ */
+ @NonNull
+ public static GotoLinkDestination convert(
+ @NonNull PdfPageGotoLinkContent.Destination pdfPageGotoLinkContentDest) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ return new GotoLinkDestination(pdfPageGotoLinkContentDest.getPageNumber(),
+ pdfPageGotoLinkContentDest.getXCoordinate(),
+ pdfPageGotoLinkContentDest.getYCoordinate(),
+ pdfPageGotoLinkContentDest.getZoom());
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
index 9d51435..640ef5c 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
@@ -18,8 +18,12 @@
import android.annotation.SuppressLint;
import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,6 +31,7 @@
import androidx.pdf.data.ListOfList;
import androidx.pdf.util.Preconditions;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -114,4 +119,68 @@
parcel.writeList(mLinkToRect);
parcel.writeList(mUrls);
}
+
+ /**
+ * Flattens the list of PdfPageLinkContent objects and converts to a LinkRects objects.
+ * <p>As an example, in case there are 2 weblinks on the page of the document with the 1st link
+ * overflowing to the next line, {@code List<PdfPageLinkContent>} would have the following
+ * values -
+ * <pre>
+ * List(
+ * PdfPageLinkContent(
+ * bounds = [RectF(l1, t1, r1, b1), RectF(l2, t2, r2, b2)],
+ * url = url1
+ * ),
+ * PdfPageLinkContent(
+ * bounds = [RectF(l3, t3, r3, b3)],
+ * url = url2
+ * ),
+ * )
+ *
+ * Using the method below, we can flatten the {@code List<PdfPageLinkContent>} to the following
+ * representation -
+ * LinkRects(
+ * mRects=[Rect(l1, t1, r1, b1), Rect(l2, t2, r2, b2), Rect(l3, t3, r3, b3)],
+ * mLinkToRect=[0,2],
+ * mUrls=[url1, url2]
+ * )
+ * </pre>
+ */
+ @NonNull
+ public static LinkRects flattenList(@NonNull List<PdfPageLinkContent> pdfPageLinkContentList) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ List<Rect> rects = new ArrayList<>();
+ List<Integer> linkToRect = new ArrayList<>();
+ List<String> urls = new ArrayList<>();
+ int numRects = 0;
+ for (PdfPageLinkContent pdfPageLinkContent : pdfPageLinkContentList) {
+ List<RectF> rectFBounds = pdfPageLinkContent.getBounds();
+ for (RectF rectF : rectFBounds) {
+ rects.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+ (int) rectF.bottom));
+ }
+ urls.add(pdfPageLinkContent.getUri().toString());
+ linkToRect.add(numRects);
+ numRects += pdfPageLinkContent.getBounds().size();
+ }
+
+ return new LinkRects(rects, linkToRect, urls);
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @NonNull
+ public List<Rect> getRects() {
+ return mRects;
+ }
+
+ @NonNull
+ public List<Integer> getLinkToRect() {
+ return mLinkToRect;
+ }
+
+ @NonNull
+ public List<String> getUrls() {
+ return mUrls;
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
index 4dade10..e12080e 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
@@ -18,8 +18,12 @@
import android.annotation.SuppressLint;
import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -27,6 +31,7 @@
import androidx.pdf.util.Preconditions;
import java.util.AbstractList;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -161,4 +166,66 @@
parcel.writeList(mMatchToRect);
parcel.writeList(mCharIndexes);
}
+
+ /**
+ * Flattens the list of PageMatchBounds objects and converts it to a MatchRects objects.
+ * <p>As an example, in case there are 2 matches on the page of the document with the 1st match
+ * overflowing to the next line, {@code List<PageMatchBounds>} would have the following values -
+ * <pre>
+ * List(
+ * PageMatchBounds(
+ * bounds = [RectF(l1, t1, r1, b1), RectF(l2, t2, r2, b2)],
+ * mTextStartIndex = 1
+ * ),
+ * PageMatchBounds(
+ * bounds = [RectF(l3, t3, r3, b3)],
+ * mTextStartIndex = 3
+ * ),
+ * )
+ *
+ * Using the method below, we can flatten the {@code List<PageMatchBounds>} to the following
+ * representation -
+ * MatchRects(
+ * mRects=[Rect(l1, t1, r1, b1), Rect(l2, t2, r2, b2), Rect(l3, t3, r3, b3)],
+ * mMatchToRect=[0,2],
+ * mCharIndexes=[1, 3]
+ * )
+ * </pre>
+ */
+ @NonNull
+ public static MatchRects flattenList(@NonNull List<PageMatchBounds> pageMatchBoundsList) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ List<Rect> rects = new ArrayList<>();
+ List<Integer> matchToRect = new ArrayList<>();
+ List<Integer> charIndexes = new ArrayList<>();
+ int numRects = 0;
+ for (PageMatchBounds pageMatchBound : pageMatchBoundsList) {
+ List<RectF> rectFBounds = pageMatchBound.getBounds();
+ for (RectF rectF : rectFBounds) {
+ rects.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+ (int) rectF.bottom));
+ }
+ matchToRect.add(numRects);
+ numRects += pageMatchBound.getBounds().size();
+ charIndexes.add(pageMatchBound.getTextStartIndex());
+ }
+ return new MatchRects(rects, matchToRect, charIndexes);
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @NonNull
+ public List<Rect> getRects() {
+ return mRects;
+ }
+
+ @NonNull
+ public List<Integer> getMatchToRect() {
+ return mMatchToRect;
+ }
+
+ @NonNull
+ public List<Integer> getCharIndexes() {
+ return mCharIndexes;
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
index 0ba0168..0e9d985 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
@@ -17,12 +17,17 @@
package androidx.pdf.models;
import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.os.Build;
import android.os.Parcel;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.pdf.data.TextSelection;
+import java.util.ArrayList;
import java.util.List;
/** Represents text selection on a particular page of a PDF. Immutable. */
@@ -82,13 +87,6 @@
}
@Override
- public String toString() {
- return String.format("PageSelection(page=%d, start=%s, stop=%s, %d rects)", mPage,
- getStart(),
- getStop(), mRects.size());
- }
-
- @Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
parcel.writeInt(mPage);
parcel.writeParcelable(getStart(), 0);
@@ -96,4 +94,35 @@
parcel.writeList(mRects);
parcel.writeString(mText);
}
+
+ /**
+ * Converts android.graphics.pdf.models.selection.PageSelection object to its
+ * androidx.pdf.aidl.PageSelection representation.
+ */
+ @NonNull
+ public static PageSelection convert(
+ @NonNull android.graphics.pdf.models.selection.PageSelection pageSelection) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ List<PdfPageTextContent> textSelections = pageSelection.getSelectedTextContents();
+
+ // TODO: Add list handling instead of taking its first element
+ String selectedText = textSelections.get(0).getText();
+
+ List<Rect> rectBounds = new ArrayList<Rect>();
+ // TODO: Add list handling instead of taking its first element
+ List<RectF> rectFBounds = textSelections.get(0).getBounds();
+ for (RectF rectF : rectFBounds) {
+ rectBounds.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+ (int) rectF.bottom));
+ }
+
+ return new PageSelection(pageSelection.getPage(),
+ SelectionBoundary.convert(pageSelection.getStart(),
+ pageSelection.getStart().getIsRtl()),
+ SelectionBoundary.convert(pageSelection.getStop(),
+ pageSelection.getStop().getIsRtl()),
+ rectBounds, selectedText);
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
index 01294ab..e8c5b98 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
@@ -18,8 +18,10 @@
import android.annotation.SuppressLint;
import android.graphics.Point;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -101,12 +103,6 @@
}
@Override
- public String toString() {
- String indexStr = (mIndex == Integer.MAX_VALUE) ? "MAX" : Integer.toString(mIndex);
- return String.format("@%s (%d,%d)", indexStr, mX, mY);
- }
-
- @Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
parcel.writeIntArray(new int[]{mIndex, mX, mY, mIsRtl ? 1 : 0});
}
@@ -135,4 +131,41 @@
result = 31 * result + (mIsRtl ? 1231 : 1237);
return result;
}
+
+ /**
+ * Converts android.graphics.pdf.models.selection.SelectionBoundary object to its
+ * androidx.pdf.aidl.SelectionBoundary representation.
+ */
+ @NonNull
+ public static SelectionBoundary convert(
+ @NonNull android.graphics.pdf.models.selection.SelectionBoundary selectionBoundary,
+ boolean isRtl) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ if (selectionBoundary.getPoint() == null) {
+ return new SelectionBoundary(selectionBoundary.getIndex(), -1, -1, isRtl);
+ }
+ return new SelectionBoundary(selectionBoundary.getIndex(),
+ selectionBoundary.getPoint().x,
+ selectionBoundary.getPoint().y, isRtl);
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ /**
+ * Converts androidx.pdf.aidl.SelectionBoundary object to its
+ * android.graphics.pdf.models.selection.SelectionBoundary representation.
+ */
+ @NonNull
+ public static android.graphics.pdf.models.selection.SelectionBoundary convert(
+ @NonNull SelectionBoundary selectionBoundary) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ if (selectionBoundary.getIndex() == -1) {
+ return new android.graphics.pdf.models.selection.SelectionBoundary(
+ new Point(selectionBoundary.getX(), selectionBoundary.getY()));
+ }
+ return new android.graphics.pdf.models.selection.SelectionBoundary(
+ selectionBoundary.getIndex());
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java
deleted file mode 100644
index e7f6c28..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2024 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.pdf.pdflib;
-
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.os.IBinder;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.pdf.data.FutureValues;
-import androidx.pdf.models.Dimensions;
-import androidx.pdf.models.GotoLink;
-import androidx.pdf.models.LinkRects;
-import androidx.pdf.models.MatchRects;
-import androidx.pdf.models.PageSelection;
-import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.models.SelectionBoundary;
-
-import java.util.List;
-
-/** Isolated Service wrapper around the PdfClient native lib, for security purposes. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentService extends Service {
-
- private static final String TAG = "PdfDocumentService";
-
- @NonNull
- @Override
- public IBinder onBind(Intent intent) {
- return new PdfDocumentRemoteImpl();
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- return super.onUnbind(intent);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- }
-
- private static class PdfDocumentRemoteImpl extends PdfDocumentRemote.Stub {
-
- private final FutureValues.BlockingCallback<Boolean> mLoaderCallback =
- new FutureValues.BlockingCallback<>();
-
- private PdfDocument mPdfDocument;
-
- PdfDocumentRemoteImpl() {
- }
-
- @Override
- public int create(ParcelFileDescriptor pfd, String password) throws RemoteException {
- mLoaderCallback.getBlocking();
- ensurePdfDestroyed();
- int fd = pfd.detachFd();
- LoadPdfResult result = PdfDocument.createFromFd(fd, password);
- if (result.isLoaded()) {
- mPdfDocument = result.getPdfDocument();
- }
- return result.getStatus().getNumber();
- }
-
- @Override
- public int numPages() {
- mLoaderCallback.getBlocking();
- return mPdfDocument.numPages();
- }
-
- @Override
- public Dimensions getPageDimensions(int pageNum) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getPageDimensions(pageNum);
- }
-
- @Override
- public String getPageText(int pageNum) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getPageText(pageNum);
- }
-
- @Override
- public List<String> getPageAltText(int pageNum) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getPageAltText(pageNum);
- }
-
- @Override
- public Bitmap renderPage(int pageNum, int pageWidth, int pageHeight,
- boolean hideTextAnnots) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.renderPageFd(pageNum, pageWidth, pageHeight, hideTextAnnots);
- }
-
- @Override
- public Bitmap renderTile(int pageNum, int tileWidth, int tileHeight, int scaledPageWidth,
- int scaledPageHeight, int left, int top, boolean hideTextAnnots) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.renderTileFd(pageNum, tileWidth, tileHeight, scaledPageWidth,
- scaledPageHeight, left, top, hideTextAnnots);
- }
-
- @Override
- public MatchRects searchPageText(int pageNum, String query) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.searchPageText(pageNum, query);
- }
-
- @Override
- public PageSelection selectPageText(int pageNum, SelectionBoundary start,
- SelectionBoundary stop) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.selectPageText(pageNum, start, stop);
- }
-
- @Override
- public LinkRects getPageLinks(int pageNum) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getPageLinks(pageNum);
- }
-
- @Override
- public List<GotoLink> getPageGotoLinks(int pageNum) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getPageGotoLinks(pageNum);
- }
-
- @Override
- public boolean isPdfLinearized() {
- mLoaderCallback.getBlocking();
- return mPdfDocument.isPdfLinearized();
- }
-
- @Override
- public int getFormType() {
- mLoaderCallback.getBlocking();
- return mPdfDocument.getFormType();
- }
-
- @Override
- protected void finalize() throws Throwable {
- mLoaderCallback.getBlocking();
- ensurePdfDestroyed();
- super.finalize();
- }
-
- private void ensurePdfDestroyed() {
- if (mPdfDocument != null) {
- try {
- mPdfDocument.destroy();
- } catch (Throwable ignored) {
- }
- }
- mPdfDocument = null;
- }
-
- @Override
- public boolean cloneWithoutSecurity(ParcelFileDescriptor destination) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.cloneWithoutSecurity(destination);
- }
-
- @Override
- public boolean saveAs(ParcelFileDescriptor destination) {
- mLoaderCallback.getBlocking();
- return mPdfDocument.saveAs(destination);
- }
- }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
similarity index 98%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
index 5ce75a3..fcd7c7a 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
similarity index 98%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
index 66fef2ba..0818d63 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
import android.graphics.Bitmap;
import android.os.ParcelFileDescriptor;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
similarity index 97%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
index 5138d2d..d9e62a5 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java
new file mode 100644
index 0000000..c6e1956
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2024 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.pdf.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.pdf.PdfRendererPreV;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.graphics.pdf.content.PdfPageImageContent;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.PdfStatus;
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.GotoLink;
+import androidx.pdf.models.LinkRects;
+import androidx.pdf.models.MatchRects;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.models.PdfDocumentRemote;
+import androidx.pdf.models.SelectionBoundary;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Isolated Service wrapper around the PdfClient native lib, for security purposes. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PdfDocumentService extends Service {
+
+ @NonNull
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new PdfDocumentRemoteImpl();
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ return super.onUnbind(intent);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ private static class PdfDocumentRemoteImpl extends PdfDocumentRemote.Stub {
+ private PdfRendererAdapter mAdapter;
+
+ PdfDocumentRemoteImpl() {
+ }
+
+ @Override
+ public int create(ParcelFileDescriptor pfd, String password) {
+ try {
+ mAdapter = new PdfRendererAdapter(pfd, password);
+ return PdfStatus.LOADED.getNumber();
+ } catch (SecurityException e) {
+ return PdfStatus.REQUIRES_PASSWORD.getNumber();
+ } catch (Exception e) {
+ return PdfStatus.PDF_ERROR.getNumber();
+ }
+ }
+
+ @Override
+ public int numPages() {
+ return mAdapter.getPageCount();
+ }
+
+ @Override
+ public Dimensions getPageDimensions(int pageNum) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ return new Dimensions(pageAdapter.getWidth(),
+ pageAdapter.getHeight());
+ }
+ }
+
+ @Override
+ public Bitmap renderPage(int pageNum, int pageWidth, int pageHeight,
+ boolean hideTextAnnots) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ Bitmap output = Bitmap.createBitmap(pageWidth, pageHeight, Bitmap.Config.ARGB_8888);
+ output.eraseColor(Color.WHITE);
+ pageAdapter.render(output);
+ return output;
+ }
+ }
+
+ @Override
+ public Bitmap renderTile(int pageNum, int tileWidth, int tileHeight, int scaledPageWidth,
+ int scaledPageHeight, int left, int top, boolean hideTextAnnots) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ Bitmap output = Bitmap.createBitmap(tileWidth, tileHeight, Bitmap.Config.ARGB_8888);
+ output.eraseColor(Color.WHITE);
+ pageAdapter.renderTile(output, left, top, scaledPageWidth, scaledPageHeight);
+ return output;
+ }
+ }
+
+
+ @Override
+ public String getPageText(int pageNum) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ List<PdfPageTextContent> textPdfContentList = pageAdapter.getPageTextContents();
+ // TODO: Add list handling instead of taking its first element
+ return textPdfContentList.get(0).getText();
+ }
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @Override
+ public List<String> getPageAltText(int pageNum) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ List<PdfPageImageContent> text = pageAdapter.getPageImageContents();
+ return text.stream().map(PdfPageImageContent::getAltText).collect(
+ Collectors.toList());
+ }
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @Override
+ public MatchRects searchPageText(int pageNum, String query) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ List<PageMatchBounds> searchResultList = pageAdapter.searchPageText(query);
+ return MatchRects.flattenList(searchResultList);
+ }
+ }
+
+ @Override
+ public PageSelection selectPageText(int pageNum, SelectionBoundary start,
+ SelectionBoundary stop) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ android.graphics.pdf.models.selection.PageSelection pageSelection =
+ pageAdapter.selectPageText(SelectionBoundary.convert(start),
+ SelectionBoundary.convert(stop));
+ if (pageSelection != null) {
+ return PageSelection.convert(pageSelection);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public LinkRects getPageLinks(int pageNum) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ List<PdfPageLinkContent> pageLinks = pageAdapter.getPageLinks();
+ return LinkRects.flattenList(pageLinks);
+ }
+ }
+
+ @Override
+ public List<GotoLink> getPageGotoLinks(int pageNum) {
+ try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+ List<PdfPageGotoLinkContent> gotoLinks = pageAdapter.getPageGotoLinks();
+ if (!gotoLinks.isEmpty()) {
+ List<GotoLink> list = new ArrayList<>();
+ for (PdfPageGotoLinkContent link : gotoLinks) {
+ GotoLink convertedLink = GotoLink.convert(link);
+ list.add(convertedLink);
+ }
+ return list;
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public boolean isPdfLinearized() {
+ return mAdapter.getDocumentLinearizationType()
+ == PdfRendererPreV.DOCUMENT_LINEARIZED_TYPE_LINEARIZED;
+ }
+
+ @Override
+ public int getFormType() {
+ return mAdapter.getDocumentFormType();
+
+ }
+
+ @Override
+ public boolean cloneWithoutSecurity(ParcelFileDescriptor destination) {
+ // TODO: Implementation pending as use-case undiscovered.
+ return true;
+ }
+
+ @Override
+ public boolean saveAs(ParcelFileDescriptor destination) {
+ // TODO: Implementation pending as use-case undiscovered.
+ return true;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ mAdapter.close();
+ mAdapter = null;
+ super.finalize();
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java
new file mode 100644
index 0000000..4d17dfe
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2024 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.pdf.service;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.pdf.PdfRenderer;
+import android.graphics.pdf.PdfRendererPreV;
+import android.graphics.pdf.RenderParams;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.graphics.pdf.content.PdfPageImageContent;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.graphics.pdf.models.selection.PageSelection;
+import android.graphics.pdf.models.selection.SelectionBoundary;
+import android.os.Build;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Supplier;
+
+import java.util.List;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PdfPageAdapter implements AutoCloseable {
+ private int mPageNum;
+ private int mHeight;
+ private int mWidth;
+
+ private PdfRenderer.Page mPdfRendererPage;
+ private PdfRendererPreV.Page mPdfRendererPreVPage;
+
+ PdfPageAdapter(@NonNull PdfRenderer pdfRenderer, int pageNum) {
+ mPageNum = pageNum;
+ mPdfRendererPage = pdfRenderer.openPage(pageNum);
+ mHeight = mPdfRendererPage.getHeight();
+ mWidth = mPdfRendererPage.getWidth();
+ }
+
+ PdfPageAdapter(@NonNull PdfRendererPreV pdfRendererPreV, int pageNum) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ mPageNum = pageNum;
+ mPdfRendererPreVPage = pdfRendererPreV.openPage(pageNum);
+ mHeight = mPdfRendererPreVPage.getHeight();
+ mWidth = mPdfRendererPreVPage.getWidth();
+ }
+ }
+
+ public int getPageNum() {
+ return mPageNum;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public void render(@NonNull Bitmap bitmap) {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ mPdfRendererPage.render(bitmap, null, null, getRenderParams());
+ } else {
+ checkAndExecute(
+ () -> mPdfRendererPreVPage.render(bitmap, null, null, getRenderParams()));
+ }
+ }
+
+ public void renderTile(@NonNull Bitmap bitmap,
+ int left, int top, int scaledPageWidth, int scaledPageHeight) {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ int pageWidth = mPdfRendererPage.getWidth();
+ int pageHeight = mPdfRendererPage.getHeight();
+ Matrix transform = getTransformationMatrix(left, top, (float) scaledPageWidth,
+ (float) scaledPageHeight, pageWidth,
+ pageHeight);
+ RenderParams renderParams = getRenderParams();
+ mPdfRendererPage.render(bitmap, null, transform, renderParams);
+ } else {
+ checkAndExecute(() -> {
+ {
+ int pageWidth = mPdfRendererPreVPage.getWidth();
+ int pageHeight = mPdfRendererPreVPage.getHeight();
+ Matrix transform = getTransformationMatrix(left, top, (float) scaledPageWidth,
+ (float) scaledPageHeight, pageWidth,
+ pageHeight);
+ RenderParams renderParams = getRenderParams();
+ mPdfRendererPreVPage.render(bitmap, null, transform, renderParams);
+ }
+ });
+ }
+ }
+
+ @NonNull
+ public List<PdfPageTextContent> getPageTextContents() {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.getTextContents();
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.getTextContents());
+ }
+
+ @NonNull
+ public List<PdfPageImageContent> getPageImageContents() {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.getImageContents();
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.getImageContents());
+ }
+
+ @Nullable
+ public PageSelection selectPageText(@NonNull SelectionBoundary start,
+ @NonNull SelectionBoundary stop) {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.selectContent(start, stop);
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.selectContent(start, stop));
+ }
+
+ @NonNull
+ public List<PageMatchBounds> searchPageText(@NonNull String query) {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.searchText(query);
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.searchText(query));
+ }
+
+ @NonNull
+ public List<PdfPageLinkContent> getPageLinks() {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.getLinkContents();
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.getLinkContents());
+ }
+
+ @NonNull
+ public List<PdfPageGotoLinkContent> getPageGotoLinks() {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ return mPdfRendererPage.getGotoLinks();
+ }
+ return checkAndExecute(() -> mPdfRendererPreVPage.getGotoLinks());
+
+ }
+
+ private Matrix getTransformationMatrix(int left, int top, float scaledPageWidth,
+ float scaledPageHeight,
+ int pageWidth, int pageHeight) {
+ Matrix matrix = new Matrix();
+ matrix.setScale(scaledPageWidth / pageWidth,
+ scaledPageHeight / pageHeight);
+ matrix.postTranslate(-left, -top);
+ return matrix;
+ }
+
+ private RenderParams getRenderParams() {
+ return checkAndExecute(() -> {
+ RenderParams.Builder renderParamsBuilder = new RenderParams.Builder(
+ RenderParams.RENDER_MODE_FOR_DISPLAY);
+ return renderParamsBuilder.setRenderFlags(0).build();
+ });
+ }
+
+ private static void checkAndExecute(@NonNull Runnable block) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ block.run();
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ private static <T> T checkAndExecute(@NonNull Supplier<T> block) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ return block.get();
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @Override
+ public void close() {
+ if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+ mPdfRendererPage.close();
+ mPdfRendererPage = null;
+ } else {
+ checkAndExecute(() -> {
+ mPdfRendererPreVPage.close();
+ mPdfRendererPreVPage = null;
+ });
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java
new file mode 100644
index 0000000..7746046
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 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.pdf.service;
+
+import android.graphics.pdf.LoadParams;
+import android.graphics.pdf.PdfRenderer;
+import android.graphics.pdf.PdfRendererPreV;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Supplier;
+
+import java.io.IOException;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PdfRendererAdapter implements AutoCloseable {
+ private PdfRenderer mPdfRenderer;
+ private PdfRendererPreV mPdfRendererPreV;
+
+ PdfRendererAdapter(@NonNull ParcelFileDescriptor parcelFileDescriptor,
+ @NonNull String password)
+ throws IOException, SecurityException {
+ if (BuildCompat.isAtLeastV()) {
+ LoadParams params = new LoadParams.Builder().setPassword(password).build();
+ mPdfRenderer = new PdfRenderer(parcelFileDescriptor, params);
+ } else {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ LoadParams params = new LoadParams.Builder().setPassword(password).build();
+ mPdfRendererPreV = new PdfRendererPreV(parcelFileDescriptor, params);
+ }
+ }
+ }
+
+ /** */
+ @NonNull
+ PdfPageAdapter openPage(int pageNum) {
+ if (mPdfRenderer != null) {
+ return new PdfPageAdapter(mPdfRenderer, pageNum);
+ }
+ return new PdfPageAdapter(mPdfRendererPreV, pageNum);
+ }
+
+ public int getPageCount() {
+ if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+ return mPdfRenderer.getPageCount();
+ }
+ return checkAndExecute(() -> mPdfRendererPreV.getPageCount());
+ }
+
+ public int getDocumentLinearizationType() {
+ if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+ return mPdfRenderer.getDocumentLinearizationType();
+ }
+ return checkAndExecute(() -> mPdfRendererPreV.getDocumentLinearizationType());
+ }
+
+ public int getDocumentFormType() {
+ if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+ return mPdfRenderer.getPdfFormType();
+ }
+ return checkAndExecute(() -> mPdfRendererPreV.getPdfFormType());
+ }
+
+ private static void checkAndExecute(@NonNull Runnable block) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ block.run();
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ private static <T> T checkAndExecute(@NonNull Supplier<T> block) {
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+ return block.get();
+ }
+ throw new UnsupportedOperationException("Operation support above S");
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (mPdfRenderer != null) {
+ mPdfRenderer.close();
+ mPdfRenderer = null;
+ } else {
+ checkAndExecute(() -> {
+ mPdfRendererPreV.close();
+ mPdfRendererPreV = null;
+ });
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
index a33af08..0133a59 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
@@ -39,8 +39,8 @@
private AnnotationUtils() {}
- /** Launches the annotation intent for a given Uri */
- public static boolean launchAnnotationIntent(@NonNull Context context, @NonNull Uri uri) {
+ /** Returns true if there is an activity that can resolve the annotation intent else false. */
+ public static boolean resolveAnnotationIntent(@NonNull Context context, @NonNull Uri uri) {
Objects.requireNonNull(context);
Objects.requireNonNull(uri);
Intent intent = getAnnotationIntent(uri);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java
deleted file mode 100644
index aa6979e..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2024 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.pdf.util;
-
-import android.graphics.Bitmap;
-import android.os.ParcelFileDescriptor;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.io.IOException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Utility to share {@link Bitmap}s across processes using a {@link android.os.Parcelable} reference
- * that can fit safely in an Intent.
- *
- * <p>A {@link BitmapParcel} wraps a {@link Bitmap} instance and exposes an output file descriptor
- * that can be used to fill in the bytes of the wrapped bitmap from any process.
- *
- * <p>Uses a piped file descriptor, and natively reads and copies bytes from the source end into the
- * {@link Bitmap}'s byte buffer. Runs on a new Thread.
- *
- * <p>Note: Only one-way transfers are implemented (write into a bitmap from any source).
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class BitmapParcel {
-
- private static final String TAG = BitmapParcel.class.getSimpleName();
- private final Bitmap mBitmap;
- private final Timer mTimer = Timer.start();
- private CountDownLatch mCountDownLatch;
-
- /**
- * Creates a BitmapParcel that allows writing bytes into the given {@link Bitmap}.
- *
- * @param bitmap the destination bitmap: its contents will be replaced by what is sent on the
- * fd.
- */
- public BitmapParcel(@NonNull Bitmap bitmap) {
- this.mBitmap = bitmap;
- }
-
- /** Opens a file descriptor that will write into the wrapped bitmap. */
- @Nullable
- public ParcelFileDescriptor openOutputFd() {
- ParcelFileDescriptor[] pipe;
- try {
- // TODO: StrictMode - close() is not explicitly called.
- pipe = ParcelFileDescriptor.createPipe();
- } catch (IOException e) {
- return null;
- }
- ParcelFileDescriptor source = pipe[0];
- ParcelFileDescriptor sink = pipe[1];
- receiveAsync(source);
- return sink;
- }
-
- /** Terminates any running copy and close all resources. */
- public void close() {
- if (mCountDownLatch != null) {
- boolean timedOut = false;
- try {
- timedOut = !mCountDownLatch.await(5, TimeUnit.SECONDS);
- } catch (InterruptedException ignored) {
- }
- }
- }
-
- /** Receives the bitmap's bytes from a file descriptor. Runs on a new thread. */
- private void receiveAsync(final ParcelFileDescriptor source) {
- mCountDownLatch = new CountDownLatch(1);
- new Thread(
- () -> {
- Timer t = Timer.start();
- receiveBitmap(mBitmap, source);
- mCountDownLatch.countDown();
- },
- "Pico-AsyncPipedFdNative.receiveAsync")
- .start();
- }
-
- /**
- * Reads bytes from a file descriptor into a {@link Bitmap}, using a native memcpy.
- *
- * @param bitmap A bitmap whose pixels to populate.
- * @param sourceFd The source file descriptor.
- */
- protected void receiveBitmap(@NonNull Bitmap bitmap, @NonNull ParcelFileDescriptor sourceFd) {
- readIntoBitmap(bitmap, sourceFd.detachFd());
- }
-
- /**
- * Reads bytes from the given file descriptor and fill in the Bitmap instance.
- *
- * @param bitmap A bitmap whose pixels to populate.
- * @param sourceFd The source file descriptor. Will be closed after transfer.
- */
- private static native boolean readIntoBitmap(Bitmap bitmap, int sourceFd);
-
- /**
- *
- */
- public static void loadNdkLib() {
- System.loadLibrary("bitmapParcel");
- }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java
new file mode 100644
index 0000000..9cf2139
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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.pdf.util;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utils class for [PaginatedView]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PaginationUtils {
+ private PaginationUtils() {
+ }
+
+ /** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
+ private static final int PAGE_ELEVATION_DP = 2;
+
+ /** Converts a value given in dp to pixels, based on the screen density. */
+ public static int getPageElevationInPixels(@NonNull Context context) {
+ float density = context.getResources().getDisplayMetrics().density;
+ return (int) (PAGE_ELEVATION_DP * density);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
index 6711671..fd9ce63 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
@@ -38,4 +38,91 @@
return outerHeight / innerHeight;
}
}
+
+ /**
+ * Used to convert the zoom view coordinates to the content coordinates using the current
+ * zoom and scroll values.
+ *
+ * @param zoomViewCoordinate coordinate for either the x or y-axis in the [ZoomView] viewport.
+ * @param zoom current zoom factor.
+ * @param scroll current scroll position.
+ * @return content coordinates converted from the viewport coordinates.
+ */
+ public static float toContentCoordinate(float zoomViewCoordinate, float zoom, int scroll) {
+ Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive.");
+ return (zoomViewCoordinate + scroll) / zoom;
+ }
+
+ /**
+ * Used to convert the content coordinates to the view coordinates using the current
+ * zoom and scroll values.
+ *
+ * @param contentCoordinate coordinate for either the x or y-axis in the [ZoomView] viewport.
+ * @param zoom current zoom factor.
+ * @param scroll current scroll position.
+ * @return view coordinates converted from the content coordinates.
+ */
+ public static float toZoomViewCoordinate(float contentCoordinate, float zoom, int scroll) {
+ Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive.");
+ return (contentCoordinate * zoom) - scroll;
+ }
+
+ /**
+ * Used to find the delta between the view port pivot and the pivot after the zoom in/out is
+ * done. Delta is positive in case of zooming in, negative in case it has been zoomed out and
+ * 0 if no change.
+ *
+ * @param oldZoom previous zoom factor.
+ * @param newZoom current zoom factor.
+ * @param zoomPivot pivot point from zoom.
+ * @param scroll scroll position.
+ * @return delta between the view port and zoomed in/out pivot.
+ */
+ public static int scrollDeltaNeededForZoomChange(
+ float oldZoom, float newZoom, float zoomPivot, int scroll) {
+ // Find where the given pivot point would move to when we change the zoom, and return the
+ // delta.
+ float contentPivot = ZoomUtils.toContentCoordinate(zoomPivot, oldZoom, scroll);
+ float movedZoomViewPivot = ZoomUtils.toZoomViewCoordinate(contentPivot, newZoom,
+ scroll);
+ return (int) (movedZoomViewPivot - zoomPivot);
+ }
+
+ /**
+ * Used to constrain the coordinate using the current zoom factor and the scroll position
+ * based on the content raw size and the view port size. In case of adjusting the x
+ * coordinate, the content and view port dimension will be the width while in case of
+ * y-coordinate it will be the width. Lower and upper bound is the left and right of the
+ * content for x-axis and is top and bottom for y-axis.
+ *
+ * @param zoom current zoom factor.
+ * @param scroll current scroll position.
+ * @param contentRawDimension raw dimension (height/width) for the content
+ * @param viewportDimension viewport dimension
+ * @return scaled coordinate or 0 if no adjustment is needed.
+ */
+ public static int constrainCoordinate(float zoom, int scroll, int contentRawDimension,
+ int viewportDimension) {
+ float lowerBound = ZoomUtils.toZoomViewCoordinate(0, zoom, scroll);
+ float upperBound = ZoomUtils.toZoomViewCoordinate(contentRawDimension, zoom, scroll);
+
+ if (lowerBound <= 0 && upperBound >= viewportDimension) {
+ // Content too large for viewport and no dead margins: no adjustment needed.
+ return 0;
+ }
+
+ float scaledContentSize = upperBound - lowerBound;
+ if (scaledContentSize <= viewportDimension) {
+ // Content fits in viewport: keep in the center.
+ return (int) ((upperBound + lowerBound - viewportDimension) / 2);
+ } else {
+ // Content doesn't fit in viewport: eliminate dead margins.
+ if (lowerBound > 0) { // Dead margin on the left.
+ return (int) lowerBound;
+ } else if (upperBound < viewportDimension) { // Dead margin on the right.
+ return (int) (upperBound - viewportDimension);
+ }
+ }
+ return 0;
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
index 11eb417..6575393 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
@@ -52,7 +52,7 @@
super(context, attrs);
}
- public AbstractPaginatedView(@NonNull Context context, @NonNull AttributeSet attrs,
+ public AbstractPaginatedView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java
new file mode 100644
index 0000000..ae535e2
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import androidx.annotation.Nullable;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.widget.FastScrollView;
+
+class FastScrollPositionValueObserver implements ObservableValue.ValueObserver<Integer> {
+ private final FastScrollView mFastScrollView;
+ private final PageIndicator mPageIndicator;
+
+ FastScrollPositionValueObserver(FastScrollView fastScrollView, PageIndicator pageIndicator) {
+ mFastScrollView = fastScrollView;
+ mPageIndicator = pageIndicator;
+ }
+
+ @Override
+ public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
+ if (mPageIndicator != null && newValue != null) {
+ mPageIndicator.getView().setY(
+ newValue - (mPageIndicator.getView().getHeight() / 2));
+ mPageIndicator.show();
+ showFastScrollView();
+ }
+ }
+
+ private void showFastScrollView() {
+ if (mFastScrollView != null) {
+ mFastScrollView.setVisible();
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java
new file mode 100644
index 0000000..5491475
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ThreadUtils;
+import androidx.pdf.viewer.loader.PdfLoader;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class LayoutHandler {
+ /** Only interact with Queue on the main thread. */
+ private final List<OnDimensCallback> mDimensCallbackQueue;
+ private final PdfLoader mPdfLoader;
+
+ /** The number of pages that have been laid out in the document. */
+ private int mPageLayoutReach = 0;
+ private int mInitialPageLayoutReach = 4;
+
+ public LayoutHandler(@NonNull PdfLoader pdfLoader) {
+ mDimensCallbackQueue = new ArrayList<>();
+ mPdfLoader = pdfLoader;
+ }
+
+ public int getPageLayoutReach() {
+ return mPageLayoutReach;
+ }
+
+ public void setPageLayoutReach(int pageLayoutReach) {
+ mPageLayoutReach = pageLayoutReach;
+ }
+
+ public int getInitialPageLayoutReach() {
+ return mInitialPageLayoutReach;
+ }
+
+ public void setInitialPageLayoutReach(int initialPageLayoutReach) {
+ mInitialPageLayoutReach = initialPageLayoutReach;
+ }
+
+ public void setInitialPageLayoutReachWithMax(int layoutReach) {
+ mInitialPageLayoutReach = Math.max(mInitialPageLayoutReach, layoutReach);
+ }
+
+ /**
+ * Lay out some pages up to some distant page. Not guaranteed to lay out any pages: maybe all
+ * pages, or at least enough pages, are already laid out.
+ */
+ public void maybeLayoutPages(int current) {
+ int peekAhead = Math.min(current + 2, 100);
+ int distantPage = Math.max(current + peekAhead, mInitialPageLayoutReach);
+ layoutPages(distantPage);
+ }
+
+ /**
+ * Lays out all the pages until {@code untilPage}, or equivalently so that {@code untilPage}s
+ * are laid out. So calling with {@code untilPage = 10} will ensure pages 0-9 are laid out.
+ *
+ * @param untilPage The upper limit of the range of pages to be laid out. Cropped to the
+ * number of pages of the document if this number was larger.
+ */
+ public void layoutPages(int untilPage) {
+ if (mPdfLoader == null) {
+ return;
+ }
+ int lastPage = Math.min(untilPage, mPdfLoader.getNumPages());
+ int requestLayoutPage = mPageLayoutReach;
+ while (requestLayoutPage < lastPage) {
+ mPdfLoader.loadPageDimensions(requestLayoutPage);
+ requestLayoutPage++;
+ }
+ }
+
+ /** */
+ public void add(@NonNull OnDimensCallback callback) {
+ mDimensCallbackQueue.add(callback);
+ }
+
+ /** */
+ public void processCallbacksInQueue(@NonNull Viewer.ViewState viewState, int pageNum) {
+ ThreadUtils.postOnUiThread(
+ () -> {
+ if (mDimensCallbackQueue.isEmpty()
+ || viewState == Viewer.ViewState.NO_VIEW) {
+ return;
+ }
+
+ Iterator<OnDimensCallback> iterator =
+ mDimensCallbackQueue.iterator();
+ while (iterator.hasNext()) {
+ OnDimensCallback callback = iterator.next();
+ boolean shouldKeep = callback.onDimensLoaded(pageNum);
+ if (!shouldKeep) {
+ iterator.remove();
+ }
+ }
+ });
+ }
+
+ /** Callback is called everytime dimensions for a page have loaded. */
+ public interface OnDimensCallback {
+ /** Return true to continue receiving callbacks, else false. */
+ boolean onDimensLoaded(int pageNum);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
index b0f95d3..17a203d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
@@ -32,6 +32,7 @@
import androidx.pdf.models.LinkRects;
import androidx.pdf.util.Accessibility;
import androidx.pdf.util.BitmapRecycler;
+import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.widget.MosaicView;
import java.util.List;
@@ -40,7 +41,6 @@
* Renders one Page.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings("UnusedVariable")
public class PageMosaicView extends MosaicView implements PageViewFactory.PageView {
@VisibleForTesting
@@ -50,19 +50,28 @@
private String mPageText;
private LinkRects mUrlLinks;
private List<GotoLink> mGotoLinks;
+ private final PdfLoader mPdfLoader;
+ private final PdfSelectionModel mSelectionModel;
+ private final SearchModel mSearchModel;
public PageMosaicView(
@NonNull Context context,
int pageNum,
@NonNull Dimensions pageSize,
@NonNull BitmapSource bitmapSource,
- @Nullable BitmapRecycler bitmapRecycler) {
+ @Nullable BitmapRecycler bitmapRecycler,
+ @NonNull PdfLoader pdfLoader,
+ @NonNull PdfSelectionModel selectionModel,
+ @NonNull SearchModel searchModel) {
super(context);
this.mPageNum = pageNum;
init(pageSize, bitmapRecycler, bitmapSource);
setId(pageNum);
setPageText(null);
setFocusableInTouchMode(true);
+ this.mPdfLoader = pdfLoader;
+ this.mSelectionModel = selectionModel;
+ this.mSearchModel = searchModel;
}
/** Set the given overlay. */
@@ -169,4 +178,38 @@
public View asView() {
return this;
}
+
+ /**
+ * Loads the page content like page text, external urls and goto links and also resets the
+ * overlays from selection and search
+ */
+ public void refreshPageContentAndOverlays() {
+ loadPageComponents();
+ resetOverlays();
+ }
+
+ /** Loads the page text, external links and the goto links for the page */
+ private void loadPageComponents() {
+ if (needsPageText()) {
+ mPdfLoader.loadPageText(mPageNum);
+ }
+ if (!hasPageUrlLinks()) {
+ mPdfLoader.loadPageUrlLinks(mPageNum);
+ }
+ if (!hasPageGotoLinks()) {
+ mPdfLoader.loadPageGotoLinks(mPageNum);
+ }
+ }
+
+ private void resetOverlays() {
+ if (getPageNum() == mSelectionModel.getPage()) {
+ setOverlay(new PdfHighlightOverlay(mSelectionModel.selection().get()));
+ } else if (mSearchModel.query().get() != null) {
+ if (!hasOverlay()) {
+ mPdfLoader.searchPageText(getPageNum(), mSearchModel.query().get());
+ }
+ } else {
+ setOverlay(null);
+ }
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
new file mode 100644
index 0000000..8936f4e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.util.Preconditions;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PageRangeHandler {
+ private static final int PAGE_PREFETCH_RADIUS = 1;
+
+ private final PaginationModel mPaginationModel;
+
+ /** The range of currently visible pages. */
+ private Range mVisiblePages = null;
+
+ /** The highest number page reached. */
+ private int mMaxPage = -1;
+
+ PageRangeHandler(PaginationModel paginationModel) {
+ this.mPaginationModel = paginationModel;
+ this.mVisiblePages = new Range();
+ }
+
+ @Nullable
+ public Range getVisiblePages() {
+ return mVisiblePages;
+ }
+
+ public void setVisiblePages(@Nullable Range visiblePages) {
+ mVisiblePages = visiblePages;
+ }
+
+ public int getMaxPage() {
+ return mMaxPage;
+ }
+
+ public void setMaxPage(int maxPage) {
+ mMaxPage = maxPage;
+ }
+
+ @NonNull
+ public PaginationModel getPaginationModel() {
+ return mPaginationModel;
+ }
+
+ /**
+ * Returns the page currently roughly centered in the view.
+ */
+ public int getVisiblePage() {
+ return (mVisiblePages != null) ? (mVisiblePages.getFirst() + mVisiblePages.getLast()) / 2
+ : 0;
+ }
+
+ /**
+ * Updates the max page to the upper bound of the visible page range if the upper bound is
+ * greater than the current max page
+ */
+ public void adjustMaxPageToUpperVisibleRange() {
+ if (mVisiblePages != null) {
+ mMaxPage = Math.max(mVisiblePages.getLast(), mMaxPage);
+ }
+ }
+
+ /** Updates the visible page range based on the y-scroll, current zoom and the view height */
+ public void refreshVisiblePageRange(int scrollY, float zoom, int viewHeight) {
+ mVisiblePages = computeVisibleRange(scrollY, zoom, viewHeight, true);
+ }
+
+ /** Computes the range of visible pages in the given position. */
+ @NonNull
+ public Range computeVisibleRange(int scrollY, float zoom, int viewHeight,
+ boolean includePartial) {
+ Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive!");
+
+ int top = Math.round(scrollY / zoom);
+ int bottom = Math.round((scrollY + viewHeight) / zoom);
+ Range window = new Range(top, bottom);
+ return mPaginationModel.getPagesInWindow(window, includePartial);
+ }
+
+ /** Returns the range of pages within the prefetch radius of the visible pages. */
+ @NonNull
+ public Range getNearPagesToVisibleRange() {
+ Range allPages = new Range(0, mPaginationModel.getSize() - 1);
+ return mVisiblePages.expand(PAGE_PREFETCH_RADIUS, allPages);
+ }
+
+ /** Returns the pages that are out of view and prefetch radius */
+ @NonNull
+ public Range[] getGonePageRanges(@NonNull Range nearPages) {
+ Range allPages = new Range(0, mPaginationModel.getSize() - 1);
+ return allPages.minus(nearPages);
+ }
+
+ /** Returns the range of pages near the visible pages that are invisible to the view port */
+ @NonNull
+ public Range[] getInvisibleNearPageRanges(@NonNull Range nearPages) {
+ return nearPages.minus(mVisiblePages);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
new file mode 100644
index 0000000..7a7f55c
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import android.content.Context;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.PaginationUtils;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PageSelectionValueObserver implements ObservableValue.ValueObserver<PageSelection> {
+ private final PaginatedView mPaginatedView;
+ private final PaginationModel mPaginationModel;
+ private final PageViewFactory mPageViewFactory;
+ private Context mContext;
+
+ PageSelectionValueObserver(PaginatedView paginatedView, PaginationModel paginationModel,
+ PageViewFactory pageViewFactory, Context context) {
+ mPaginatedView = paginatedView;
+ mPaginationModel = paginationModel;
+ mPageViewFactory = pageViewFactory;
+ mContext = context;
+ }
+
+ @Override
+ public void onChange(PageSelection oldSelection, PageSelection newSelection) {
+ if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+ getPage(oldSelection.getPage()).getPageView().setOverlay(null);
+ }
+
+ Range visiblePageRange =
+ mPaginatedView.getPageRangeHandler().getVisiblePages();
+ if (newSelection != null && visiblePageRange.contains(
+ newSelection.getPage())) {
+ ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
+ newSelection.getPage(),
+ PaginationUtils.getPageElevationInPixels(mContext),
+ mPaginationModel.getPageSize(newSelection.getPage())))
+ .setOverlay(new PdfHighlightOverlay(newSelection));
+ }
+ }
+
+ private boolean isPageCreated(int pageNum) {
+ return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ }
+
+ private PageViewFactory.PageView getPage(int pageNum) {
+ return mPaginatedView.getViewAt(pageNum);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
index 0d0730f..6a802cd 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
@@ -20,25 +20,28 @@
import android.view.MotionEvent;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.pdf.models.SelectionBoundary;
import androidx.pdf.util.GestureTracker;
import androidx.pdf.viewer.loader.PdfLoader;
/** Gesture listener for PageView's handling of tap and long press. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class PageTouchListener extends GestureTracker.GestureHandler {
private final PageViewFactory.PageView mPageView;
private final PdfLoader mPdfLoader;
- private final PdfViewer.PageTouchHandler mPageTouchHandler;
+
+ private final SingleTapHandler mSingleTapHandler;
PageTouchListener(@NonNull PageViewFactory.PageView pageView,
@NonNull PdfLoader pdfLoader,
- @NonNull PdfViewer.PageTouchHandler pageTouchHandler) {
+ @NonNull SingleTapHandler singleTapHandler) {
this.mPageView = pageView;
this.mPdfLoader = pdfLoader;
- this.mPageTouchHandler = pageTouchHandler;
+ this.mSingleTapHandler = singleTapHandler;
}
@Override
@@ -48,7 +51,8 @@
@Override
public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
- return mPageTouchHandler.handleSingleTapNoFormFilling(e, mPageView.getPageView());
+ mSingleTapHandler.handleSingleTapConfirmedEventOnPage(e, mPageView.getPageView());
+ return true;
}
@Override
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
index efd550f..f328ed9 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
@@ -29,7 +29,6 @@
import androidx.pdf.models.LinkRects;
import androidx.pdf.util.Accessibility;
import androidx.pdf.util.GestureTracker;
-import androidx.pdf.util.ObservableValue;
import androidx.pdf.util.TileBoard;
import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.widget.MosaicView;
@@ -53,16 +52,19 @@
private final Context mContext;
private final PdfLoader mPdfLoader;
private final PaginatedView mPaginatedView;
- private final ObservableValue<ZoomView.ZoomScroll> mZoomScroll;
+ private final ZoomView mZoomView;
+ private final SingleTapHandler mSingleTapHandler;
public PageViewFactory(@NonNull Context context,
@NonNull PdfLoader pdfLoader,
@NonNull PaginatedView paginatedView,
- @NonNull ZoomView zoomView) {
+ @NonNull ZoomView zoomView,
+ @NonNull SingleTapHandler singleTapHandler) {
this.mContext = context;
this.mPdfLoader = pdfLoader;
this.mPaginatedView = paginatedView;
- this.mZoomScroll = zoomView.zoomScroll();
+ this.mZoomView = zoomView;
+ this.mSingleTapHandler = singleTapHandler;
}
/**
@@ -98,20 +100,21 @@
void clearAll();
}
- /** Returns an instance of {@link PageView}. If the view is already created and added to the
- * {@link PaginatedView} then it will be returned from that list else a new instance will be
- * created. */
+ /**
+ * Returns an instance of {@link PageView}. If the view is already created and added to the
+ * {@link PaginatedView} then it will be returned from that list else a new instance will be
+ * created.
+ */
@NonNull
public PageView getOrCreatePageView(int pageNum,
int pageElevationInPixels,
- @NonNull Dimensions pageDimensions,
- @NonNull PdfViewer.PageTouchHandler handler) {
+ @NonNull Dimensions pageDimensions) {
PageView pageView = mPaginatedView.getViewAt(pageNum);
if (pageView != null) {
return pageView;
}
- return createAndSetupPageView(pageNum, pageElevationInPixels, pageDimensions, handler);
+ return createAndSetupPageView(pageNum, pageElevationInPixels, pageDimensions);
}
/**
@@ -126,9 +129,10 @@
final MosaicView.BitmapSource bitmapSource = createBitmapSource(pageNum);
final PageMosaicView pageMosaicView =
new PageMosaicView(mContext, pageNum, pageSize, bitmapSource,
- TileBoard.DEFAULT_RECYCLER);
+ TileBoard.DEFAULT_RECYCLER, mPdfLoader, mPaginatedView.getSelectionModel(),
+ mPaginatedView.getSearchModel());
if (isTouchExplorationEnabled(mContext)) {
- final PageLinksView pageLinksView = new PageLinksView(mContext, mZoomScroll);
+ final PageLinksView pageLinksView = new PageLinksView(mContext, mZoomView.zoomScroll());
return new AccessibilityPageWrapper(
mContext, pageNum, pageMosaicView, pageLinksView);
@@ -168,8 +172,7 @@
@NonNull
protected PageView createAndSetupPageView(int pageNum,
int pageElevationInPixels,
- @NonNull Dimensions pageDimensions,
- @NonNull PdfViewer.PageTouchHandler handler) {
+ @NonNull Dimensions pageDimensions) {
PageView pageView =
createPageView(
pageNum,
@@ -178,8 +181,9 @@
mPaginatedView.addView(pageView);
GestureTracker gestureTracker = new GestureTracker(mContext);
+ gestureTracker.setDelegateHandler(new PageTouchListener(pageView, mPdfLoader,
+ mSingleTapHandler));
pageView.asView().setOnTouchListener(gestureTracker);
- gestureTracker.setDelegateHandler(new PageTouchListener(pageView, mPdfLoader, handler));
PageMosaicView pageMosaicView = pageView.getPageView();
// Setting Elevation only works if there is a background color.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 35bd5b8..f1ae9f2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -25,8 +25,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.util.PaginationUtils;
import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.ThreadUtils;
import androidx.pdf.viewer.PageViewFactory.PageView;
+import androidx.pdf.viewer.loader.PdfLoader;
+import androidx.pdf.widget.ZoomView;
import java.util.AbstractList;
import java.util.List;
@@ -46,16 +51,72 @@
/** Maps the current child views to pages. */
private final SparseArray<PageView> mPageViews = new SparseArray<>();
+ private PaginationModel mPaginationModel;
+
+ private PageRangeHandler mPageRangeHandler;
+
+ private PdfSelectionModel mSelectionModel;
+
+ private SearchModel mSearchModel;
+
+ private PdfLoader mPdfLoader;
+
+ private PageViewFactory mPageViewFactory;
+
public PaginatedView(@NonNull Context context) {
- super(context);
+ this(context, null);
}
- public PaginatedView(@NonNull Context context, @NonNull AttributeSet attrs) {
- super(context, attrs);
+ public PaginatedView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
}
- public PaginatedView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyle) {
+ public PaginatedView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mPaginationModel = new PaginationModel();
+ mPageRangeHandler = new PageRangeHandler(mPaginationModel);
+ }
+
+ @NonNull
+ public PaginationModel getPaginationModel() {
+ return mPaginationModel;
+ }
+
+ @NonNull
+ public PageRangeHandler getPageRangeHandler() {
+ return mPageRangeHandler;
+ }
+
+ @NonNull
+ public PdfSelectionModel getSelectionModel() {
+ return mSelectionModel;
+ }
+
+ public void setSelectionModel(
+ @NonNull PdfSelectionModel selectionModel) {
+ mSelectionModel = selectionModel;
+ }
+
+ @NonNull
+ public SearchModel getSearchModel() {
+ return mSearchModel;
+ }
+
+ public void setSearchModel(@NonNull SearchModel searchModel) {
+ mSearchModel = searchModel;
+ }
+
+ public void setPdfLoader(@NonNull PdfLoader pdfLoader) {
+ mPdfLoader = pdfLoader;
+ }
+
+ @NonNull
+ public PageViewFactory getPageViewFactory() {
+ return mPageViewFactory;
+ }
+
+ public void setPageViewFactory(@NonNull PageViewFactory pageViewFactory) {
+ mPageViewFactory = pageViewFactory;
}
/** Instantiate a page of this pageView into a child pageView. */
@@ -200,4 +261,165 @@
// laid out already for this viewArea.
onLayout(false, getLeft(), getTop(), getRight(), getBottom());
}
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ if (getVisibility() == View.VISIBLE) {
+ mPageRangeHandler.adjustMaxPageToUpperVisibleRange();
+ if (getChildCount() > 0) {
+ for (PageMosaicView page : getChildViews()) {
+ page.clearTiles();
+ if (mPdfLoader != null) {
+ mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPageRangeHandler.setVisiblePages(null);
+ }
+
+ /**
+ * Refreshes the page range for the visible area.
+ */
+ public void refreshPageRangeInVisibleArea(@NonNull ZoomView.ZoomScroll zoomScroll,
+ int parentViewHeight) {
+ mPageRangeHandler.refreshVisiblePageRange(zoomScroll.scrollY, zoomScroll.zoom,
+ parentViewHeight);
+
+ mPageRangeHandler.adjustMaxPageToUpperVisibleRange();
+ }
+
+ /** Cancels the background jobs for the disappeared pages and optionally clears the views */
+ public void handleGonePages(boolean clearViews) {
+ Range nearPages = mPageRangeHandler.getNearPagesToVisibleRange();
+ Range[] gonePages = mPageRangeHandler.getGonePageRanges(nearPages);
+ for (Range pages : gonePages) {
+ // Keep Views around for now, we'll clear them in step (4) if applicable.
+ clearPages(pages, clearViews);
+ }
+ }
+
+ /** Computes the invisible page range and loads them */
+ public void loadInvisibleNearPageRange(
+ float stableZoom) {
+ Range nearPages = mPageRangeHandler.getNearPagesToVisibleRange();
+ Range[] invisibleNearPages = mPageRangeHandler.getInvisibleNearPageRanges(nearPages);
+
+ for (Range pages : invisibleNearPages) {
+ loadPageRange(pages, stableZoom);
+ }
+ }
+
+ /**
+ * Creates the page views for the visible page range.
+ *
+ * @return true if any new page was created else false
+ */
+ public boolean createPageViewsForVisiblePageRange() {
+ boolean requiresLayoutPass = false;
+ for (int pageNum : mPageRangeHandler.getVisiblePages()) {
+ if (getViewAt(pageNum) == null) {
+ mPageViewFactory.getOrCreatePageView(pageNum,
+ PaginationUtils.getPageElevationInPixels(getContext()),
+ mPaginationModel.getPageSize(pageNum));
+ requiresLayoutPass = true;
+ }
+ }
+ return requiresLayoutPass;
+ }
+
+ /** */
+ public void refreshVisiblePages(boolean requiresLayoutPass,
+ @NonNull Viewer.ViewState viewState,
+ float stableZoom) {
+ if (requiresLayoutPass) {
+ refreshPagesAfterLayout(viewState, mPageRangeHandler.getVisiblePages(),
+ stableZoom);
+ } else {
+ refreshPages(mPageRangeHandler.getVisiblePages(), stableZoom);
+ }
+ handleGonePages(/* clearViews= */ true);
+ }
+
+ /** */
+ public void refreshVisibleTiles(boolean requiresLayoutPass,
+ @NonNull Viewer.ViewState viewState) {
+ if (requiresLayoutPass) {
+ refreshTilesAfterLayout(viewState, mPageRangeHandler.getVisiblePages());
+ } else {
+ refreshTiles(mPageRangeHandler.getVisiblePages());
+ }
+ }
+
+ private void clearPages(Range pages, boolean clearViews) {
+ for (int page : pages) {
+ // Don't cancel search - search results for the current search are always useful,
+ // even for pages we can't see right now. Form filling operations should always
+ // be executed against the document, even if the user has scrolled away from the page.
+ mPdfLoader.cancelExceptSearchAndFormFilling(page);
+ if (clearViews) {
+ removeViewAt(page);
+ }
+ }
+ }
+
+ private void loadPageRange(Range pages,
+ float stableZoom) {
+ for (int page : pages) {
+ mPdfLoader.cancelAllTileBitmaps(page);
+ PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+ page,
+ PaginationUtils.getPageElevationInPixels(getContext()),
+ mPaginationModel.getPageSize(page));
+ pageView.clearTiles();
+ pageView.requestFastDrawAtZoom(stableZoom);
+ pageView.refreshPageContentAndOverlays();
+ }
+ }
+
+ private void refreshPages(Range pages, float stableZoom) {
+ for (int page : pages) {
+ PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+ page,
+ PaginationUtils.getPageElevationInPixels(getContext()),
+ mPaginationModel.getPageSize(page));
+ pageView.requestDrawAtZoom(stableZoom);
+ pageView.refreshPageContentAndOverlays();
+ }
+ }
+
+ private void refreshPagesAfterLayout(Viewer.ViewState viewState, Range pages,
+ float stableZoom) {
+ ThreadUtils.postOnUiThread(
+ () -> {
+ if (viewState != Viewer.ViewState.NO_VIEW) {
+ refreshPages(pages, stableZoom);
+ }
+ });
+ }
+
+ private void refreshTiles(Range pages) {
+ for (int page : pages) {
+ PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+ page,
+ PaginationUtils.getPageElevationInPixels(getContext()),
+ mPaginationModel.getPageSize(page));
+ pageView.requestTiles();
+ }
+ }
+
+ private void refreshTilesAfterLayout(Viewer.ViewState viewState, Range pages) {
+ ThreadUtils.postOnUiThread(
+ () -> {
+ if (viewState != Viewer.ViewState.NO_VIEW) {
+ refreshTiles(pages);
+ }
+ });
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index 7d8e888..f0205c3 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -26,12 +26,10 @@
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
-import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnScrollChangedListener;
@@ -62,24 +60,20 @@
import androidx.pdf.find.MatchCount;
import androidx.pdf.models.Dimensions;
import androidx.pdf.models.GotoLink;
-import androidx.pdf.models.GotoLinkDestination;
import androidx.pdf.models.LinkRects;
import androidx.pdf.models.MatchRects;
import androidx.pdf.models.PageSelection;
import androidx.pdf.util.AnnotationUtils;
import androidx.pdf.util.CycleRange;
-import androidx.pdf.util.ExternalLinks;
import androidx.pdf.util.ObservableValue;
import androidx.pdf.util.ObservableValue.ValueObserver;
import androidx.pdf.util.Preconditions;
import androidx.pdf.util.Screen;
-import androidx.pdf.util.StrictModeUtils;
import androidx.pdf.util.ThreadUtils;
import androidx.pdf.util.TileBoard;
import androidx.pdf.util.TileBoard.TileInfo;
import androidx.pdf.util.Toaster;
import androidx.pdf.util.Uris;
-import androidx.pdf.util.ZoomUtils;
import androidx.pdf.viewer.PageViewFactory.PageView;
import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.viewer.loader.PdfLoaderCallbacks;
@@ -96,8 +90,6 @@
import com.google.android.material.snackbar.Snackbar;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
-import java.util.ArrayList;
-import java.util.Iterator;
import java.util.List;
/**
@@ -125,22 +117,16 @@
* connected.
* </ol>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressWarnings({"UnusedMethod", "UnusedVariable"})
public class PdfViewer extends LoadingViewer implements FastScrollContentModel {
private static final String TAG = "PdfViewer";
- @NonNull
- @Override
- protected String getLogTag() {
- return TAG;
- }
-
/** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
private static final int PAGE_ELEVATION_DP = 2;
- /** Key for saving {@link #mPageLayoutReach} in bundles. */
+ /** Key for saving page layout reach in bundles. */
private static final String KEY_LAYOUT_REACH = "plr";
private static final String KEY_SPACE_LEFT = "leftSpace";
@@ -150,9 +136,6 @@
private static final String KEY_QUIT_ON_ERROR = "quitOnError";
private static final String KEY_EXIT_ON_CANCEL = "exitOnCancel";
- /** Key to save/retrieve {@link #mEditingAuthorized} from Bundle. */
- private static final String KEY_EDITING_AUTHORIZED = "editingAuthorized";
-
private static Screen sScreen;
/** Single access to the PDF document: loads contents asynchronously (bitmaps, text,...) */
@@ -166,7 +149,7 @@
public final PdfLoaderCallbacks mPdfLoaderCallbacks;
/** Observer of the page position that controls loading of relevant PDF assets. */
- private final ValueObserver<ZoomScroll> mZoomScrollObserver;
+ private ValueObserver<ZoomScroll> mZoomScrollObserver;
/** Observer to be set when the view is created. */
@Nullable
@@ -174,23 +157,6 @@
private Object mScrollPositionObserverKey;
- /** The number of pages of this PDF, set to -1 when not available. */
- private int mNumPages = -1;
-
- /** The range of currently visible pages. */
- private Range mVisiblePages;
-
- /** The highest number page reached. */
- private int mMaxPage = -1;
-
- private int mInitialPageLayoutReach = 4;
-
- /** The number of pages that have been laid out in the document. */
- private int mPageLayoutReach;
-
- /** The last stable zoom: we only re-draw bitmaps at stable zoom (not during a gesture). */
- private float mStableZoom;
-
private ZoomView mZoomView;
private PaginatedView mPaginatedView;
@@ -201,15 +167,18 @@
private SearchModel mSearchModel;
private PdfSelectionModel mSelectionModel;
private PdfSelectionHandles mSelectionHandles;
- private final ValueObserver<String> mSearchQueryObserver;
- private final ValueObserver<SelectedMatch> mSelectedMatchObserver;
- private final ValueObserver<PageSelection> mSelectionObserver;
- private final ValueObserver<Integer> mFastscrollerPositionObserver;
+
+ private ValueObserver<String> mSearchQueryObserver;
+ private ValueObserver<Integer> mFastscrollerPositionObserver;
+ private ValueObserver<SelectedMatch> mSelectedMatchObserver;
+ private ValueObserver<PageSelection> mSelectionObserver;
private Object mFastscrollerPositionObserverKey;
private FastScrollView mFastScrollView;
private ProgressBar mLoadingSpinner;
private boolean mDocumentLoaded = false;
+ private boolean mIsAnnotationIntentResolvable = false;
+
/**
* After the document content is saved over the original in InkActivity, we set this bit to true
* so we know to callwhen the new document content is loaded.
@@ -224,15 +193,10 @@
@Nullable
private SettableFutureValue<Boolean> mSaveAsCallback;
- // Base padding for ZoomView in px as set in saveZoomViewBasePadding().
- private Rect mZoomViewBasePadding = new Rect();
- private boolean mZoomViewBasePaddingSaved;
private Snackbar mSnackbar;
- private boolean mWaitingOnSelectionToCreateInlineComment;
- private boolean mEditingAuthorized;
- /** Only interact with Queue on the main thread. */
- private final List<OnDimensCallback> mDimensCallbackQueue = new ArrayList<>();
+ private LayoutHandler mLayoutHandler;
+
private Uri mLocalUri;
private FrameLayout mPdfViewer;
@@ -242,11 +206,7 @@
private PageViewFactory mPageViewFactory;
- /** Callback is called everytime dimensions for a page have loaded. */
- private interface OnDimensCallback {
- /** Return true to continue receiving callbacks, else false. */
- boolean onDimensLoaded(int pageNum);
- }
+ private SingleTapHandler mSingleTapHandler;
public PdfViewer() {
super(SELF_MANAGED_CONTENTS);
@@ -289,15 +249,16 @@
@NonNull
@SuppressLint("InflateParams")
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container,
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedState) {
super.onCreateView(inflater, container, savedState);
- mPaginationModel = new PaginationModel();
mPdfViewer = (FrameLayout) inflater.inflate(R.layout.pdf_viewer_container, container,
false);
mFindInFileView = mPdfViewer.findViewById(R.id.search);
mFastScrollView = mPdfViewer.findViewById(R.id.fast_scroll_view);
+ mPaginatedView = mFastScrollView.findViewById(R.id.pdf_view);
+ mPaginationModel = mPaginatedView.getPaginationModel();
mZoomView = mFastScrollView.findViewById(R.id.zoom_view);
mZoomView.setStraightenVerticalScroll(true);
@@ -312,14 +273,12 @@
// predictable. An alternative that doesn't require id is to rely on this Fragment's
// onSaveInstanceState().
mZoomView.setId(getId() * 100);
- mPaginatedView = mFastScrollView.findViewById(R.id.pdf_view);
-
- mVisiblePages = new Range();
- mPageLayoutReach = 0;
mPageIndicator = new PageIndicator(getActivity(), mFastScrollView);
applyReservedSpace();
- adjustZoomViewMargins();
+ mZoomView.adjustZoomViewMargins();
+ mFastscrollerPositionObserver =
+ new FastScrollPositionValueObserver(mFastScrollView, mPageIndicator);
mFastscrollerPositionObserver.onChange(null, mFastScrollView.getScrollerPositionY().get());
mFastscrollerPositionObserverKey =
mFastScrollView.getScrollerPositionY().addObserver(mFastscrollerPositionObserver);
@@ -332,7 +291,6 @@
mFastScrollView.setId(getId() * 10);
mLoadingSpinner = mFastScrollView.findViewById(R.id.progress_indicator);
-
setUpEditFab();
return mPdfViewer;
@@ -350,7 +308,7 @@
private void applyReservedSpace() {
if (getArguments().containsKey(KEY_SPACE_TOP)) {
- saveZoomViewBasePadding();
+ mZoomView.saveZoomViewBasePadding();
int left = getArguments().getInt(KEY_SPACE_LEFT, 0);
int top = getArguments().getInt(KEY_SPACE_TOP, 0);
int right = getArguments().getInt(KEY_SPACE_RIGHT, 0);
@@ -358,11 +316,7 @@
mPageIndicator.getView().setTranslationX(-right);
- mZoomView.setPadding(
- mZoomViewBasePadding.left + left,
- mZoomViewBasePadding.top + top,
- mZoomViewBasePadding.right + right,
- mZoomViewBasePadding.bottom + bottom);
+ mZoomView.setPaddingWithBase(left, top, right, bottom);
// Adjust the scroll bar to also include the same padding.
mFastScrollView.setScrollbarMarginTop(mZoomView.getPaddingTop());
@@ -371,62 +325,9 @@
}
}
- /**
- * Saves the padding set on {@link ZoomView} following initial inflation from XML.
- *
- * <p>This does not have to be called immediately following inflation but <i>must</i> be called
- * before any methods change the padding on {@link ZoomView}.
- *
- * <p>This can be used by methods that need to set padding to (base padding + some other
- * dimension). If these values were obtained directly from {@link ZoomView} or this method was
- * allowed to execute multiple times it could result in padding expanding continually.
- */
- private void saveZoomViewBasePadding() {
- if (mZoomView == null || mZoomViewBasePaddingSaved) {
- return;
- }
-
- mZoomViewBasePadding =
- new Rect(
- mZoomView.getPaddingLeft(),
- mZoomView.getPaddingTop(),
- mZoomView.getPaddingRight(),
- mZoomView.getPaddingBottom());
-
- mZoomViewBasePadding.top +=
- getResources().getDimensionPixelSize(R.dimen.viewer_doc_additional_top_offset);
-
- mZoomViewBasePaddingSaved = true;
- }
-
- /**
- * Adjusts the horizontal margins (left and right padding) of the ZoomView based on the
- * screen width to optimize the display of PDF content.
- *
- * This method applies different margin values depending on the screen size:
- * - For screens with a screen width of 840dp or greater, a larger margin is applied
- * to enhance readability on larger displays.
- * - For screens with a screen width < 840dp, no margin is used to
- * maximize the use of available space.
- *
- * This dynamic adjustment is achieved through the use of resource qualifiers (values-w840dp)
- * that define different margin values for different screen sizes.
- *
- * Note: This method does not affect the top or bottom padding of the ZoomView.
- */
- private void adjustZoomViewMargins() {
- int margin = getResources().getDimensionPixelSize(R.dimen.viewer_doc_padding_x);
-
- mZoomView.setPadding(margin,
- mZoomView.getPaddingTop(),
- margin,
- mZoomView.getPaddingBottom());
- }
-
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- mZoomView.zoomScroll().addObserver(mZoomScrollObserver);
if (mPendingScrollPositionObserver != null) {
mScrollPositionObserverKey = mZoomView.zoomScroll().addObserver(
mPendingScrollPositionObserver);
@@ -437,9 +338,8 @@
@Override
protected void onContentsAvailable(@NonNull DisplayData contents, @Nullable Bundle savedState) {
mFileData = contents;
+ mLocalUri = contents.getUri();
- // TODO: StrictMode- disk read 58ms.
- int lengthMb = StrictModeUtils.bypassAndReturn(() -> (int) (contents.length() >> 20));
createContentModel(
PdfLoader.create(
getActivity().getApplicationContext(),
@@ -447,11 +347,40 @@
TileBoard.DEFAULT_RECYCLER,
mPdfLoaderCallbacks,
false));
+ mLayoutHandler = new LayoutHandler(mPdfLoader);
+ mZoomView.setPdfSelectionModel(mSelectionModel);
+ mPaginatedView.setSelectionModel(mSelectionModel);
+ mPaginatedView.setSearchModel(mSearchModel);
+ mPaginatedView.setPdfLoader(mPdfLoader);
+
+ mSearchQueryObserver =
+ new SearchQueryObserver(mPaginatedView);
+ mSearchModel.query().addObserver(mSearchQueryObserver);
+
+ mZoomScrollObserver =
+ new ZoomScrollValueObserver(mZoomView, mPaginatedView,
+ mLayoutHandler, mAnnotationButton, mFindInFileView, mPageIndicator,
+ mFastScrollView, mIsAnnotationIntentResolvable, mViewState);
+ mZoomView.zoomScroll().addObserver(mZoomScrollObserver);
+ mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton,
+ mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler);
+ mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
+ mPaginatedView, mZoomView, mSingleTapHandler);
+ mPaginatedView.setPageViewFactory(mPageViewFactory);
+
+ mSelectionObserver =
+ new PageSelectionValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ requireContext());
+ mSelectionModel.selection().addObserver(mSelectionObserver);
+
+ mSelectedMatchObserver =
+ new SelectedMatchValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ mZoomView, mLayoutHandler, requireContext());
+ mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
if (savedState != null) {
int layoutReach = savedState.getInt(KEY_LAYOUT_REACH);
- mEditingAuthorized = savedState.getBoolean(KEY_EDITING_AUTHORIZED);
- mInitialPageLayoutReach = Math.max(mInitialPageLayoutReach, layoutReach);
+ mLayoutHandler.setInitialPageLayoutReachWithMax(layoutReach);
}
}
@@ -465,9 +394,6 @@
mPdfLoader.reconnect();
}
- mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
- mPaginatedView, mZoomView);
-
if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
loadPageAssets(mZoomView.zoomScroll().get());
}
@@ -475,35 +401,19 @@
@Override
public void onExit() {
- if (mVisiblePages != null && mVisiblePages.getLast() > mMaxPage) {
- mMaxPage = mVisiblePages.getLast();
- }
-
super.onExit();
if (!mDocumentLoaded && mPdfLoader != null) {
// e.g. a password-protected pdf that wasn't loaded.
mPdfLoader.disconnect();
}
-
- if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
- for (PageMosaicView page : mPaginatedView.getChildViews()) {
- page.clearTiles();
- if (mPdfLoader != null) {
- mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
- }
- }
- }
}
private void createContentModel(PdfLoader pdfLoader) {
this.mPdfLoader = pdfLoader;
mSearchModel = new SearchModel(pdfLoader);
- mSearchModel.query().addObserver(mSearchQueryObserver);
- mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
mSelectionModel = new PdfSelectionModel(pdfLoader);
- mSelectionModel.selection().addObserver(mSelectionObserver);
mSelectionHandles = new PdfSelectionHandles(mSelectionModel, mZoomView, mPaginatedView);
@@ -544,6 +454,8 @@
if (mScrollPositionObserverKey != null) {
mZoomView.zoomScroll().removeObserver(mScrollPositionObserverKey);
}
+ mZoomView.setZoomViewBasePadding(new Rect());
+ mZoomView.setZoomViewBasePaddingSaved(false);
mZoomView = null;
}
@@ -552,17 +464,12 @@
mPaginationModel.removeObserver(mPaginatedView);
mPaginatedView = null;
}
- // Clears the model so we can start fresh if we rebuild views.
- mPaginationModel = new PaginationModel();
- mVisiblePages = null;
if (mPdfLoader != null) {
mPdfLoader.cancelAll();
mPdfLoader.disconnect();
mDocumentLoaded = false;
}
- mZoomViewBasePadding = new Rect();
- mZoomViewBasePaddingSaved = false;
super.destroyView();
}
@@ -588,28 +495,13 @@
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
- outState.putInt(KEY_LAYOUT_REACH, mPageLayoutReach);
-
- outState.putBoolean(KEY_EDITING_AUTHORIZED, mEditingAuthorized);
+ outState.putInt(KEY_LAYOUT_REACH, mLayoutHandler.getPageLayoutReach());
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
- adjustZoomViewMargins();
- }
-
- @Override
- public long getContentLength() {
- return getPageCount();
- }
-
- @Override
- public int getViewProgress() {
- if (mMaxPage > 0) {
- return (int) (((double) mMaxPage / getPageCount() * 100) * PROGRESS_SCALER);
- }
- return -1;
+ mZoomView.adjustZoomViewMargins();
}
/**
@@ -629,6 +521,9 @@
showSpinner();
fetchFile(fileUri);
mLocalUri = fileUri;
+ mIsAnnotationIntentResolvable = AnnotationUtils.resolveAnnotationIntent(requireContext(),
+ mLocalUri);
+ mSingleTapHandler.setAnnotationIntentResolvable(mIsAnnotationIntentResolvable);
}
private void validateFileUri(Uri fileUri) {
@@ -708,45 +603,6 @@
postEnter();
}
- private int getPageCount() {
- return mNumPages;
- }
-
- /** Returns the page currently roughly centered in the view. */
- public int getViewingPage() {
- return (mVisiblePages != null) ? (mVisiblePages.getFirst() + mVisiblePages.getLast()) / 2
- : 0;
- }
-
- /**
- * Lay out some pages up to some distant page. Not guaranteed to lay out any pages: maybe all
- * pages, or at least enough pages, are already laid out.
- */
- private void maybeLayoutPages(int current) {
- int peekAhead = Math.min(current + 2, 100);
- int distantPage = Math.max(current + peekAhead, mInitialPageLayoutReach);
- layoutPages(distantPage);
- }
-
- /**
- * Lays out all the pages until {@code untilPage}, or equivalently so that {@code untilPage}s
- * are laid out. So calling with {@code untilPage = 10} will ensure pages 0-9 are laid out.
- *
- * @param untilPage The upper limit of the range of pages to be laid out. Cropped to the
- * number of pages of the document if this number was larger.
- */
- private void layoutPages(int untilPage) {
- if (mPdfLoader == null) {
- return;
- }
- int lastPage = Math.min(untilPage, getPageCount());
- int requestLayoutPage = mPageLayoutReach;
- while (requestLayoutPage < lastPage) {
- mPdfLoader.loadPageDimensions(requestLayoutPage);
- requestLayoutPage++;
- }
- }
-
private boolean isPageCreated(int pageNum) {
return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
}
@@ -755,16 +611,12 @@
return mPaginatedView.getViewAt(pageNum);
}
- private Range allPages() {
- return new Range(0, mPaginationModel.getSize() - 1);
- }
-
private void lookAtSelection(SelectedMatch selection) {
if (selection == null || selection.isEmpty()) {
return;
}
if (selection.getPage() >= mPaginationModel.getSize()) {
- layoutPages(selection.getPage() + 1);
+ mLayoutHandler.layoutPages(selection.getPage() + 1);
return;
}
Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
@@ -775,148 +627,38 @@
PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
selection.getPage(),
sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(selection.getPage()),
- new PageTouchHandler());
+ mPaginationModel.getPageSize(selection.getPage()));
pageView.setOverlay(selection.getOverlay());
}
private void loadPageAssets(ZoomScroll position) {
- Range oldVisiblePages = mVisiblePages;
-
- // 1. Refresh visible pages and view area.
- mVisiblePages = computeVisibleRange(position);
-
- if (mVisiblePages.getLast() > mMaxPage) {
- mMaxPage = mVisiblePages.getLast();
- }
-
// Change the resolution of the bitmaps only when a gesture is not in progress.
- if (position.stable || mStableZoom == 0) {
- mStableZoom = position.zoom;
+ if (position.stable || mZoomView.getStableZoom() == 0) {
+ mZoomView.setStableZoom(position.zoom);
}
mPaginationModel.setViewArea(mZoomView.getVisibleAreaInContentCoords());
-
- Range allPages = allPages();
- int prefetchRadius = 1; // Note: could make this variable depending on resolution.
- Range nearPages = expandRange(mVisiblePages, prefetchRadius, allPages);
-
- // 2. Release pages that we don't need anymore.
- Range[] gonePages = allPages.minus(nearPages);
- for (Range pages : gonePages) {
- // Keep Views around for now, we'll clear them in step (4) if applicable.
- clearPages(pages, false);
- }
-
- // 3. Bring minimal service to pages that we might need very soon
- Range[] invisibleNearPages = nearPages.minus(mVisiblePages);
- for (Range pages : invisibleNearPages) {
- loadPageOnly(pages);
- }
+ mPaginatedView.refreshPageRangeInVisibleArea(position, mZoomView.getHeight());
+ mPaginatedView.handleGonePages(/* clearViews= */ false);
+ mPaginatedView.loadInvisibleNearPageRange(mZoomView.getStableZoom());
// The step (4) below requires page Views to be created and laid out. So we create them here
// and set this flag if that operation needs to wait for a layout pass.
- boolean requiresLayoutPass = false;
- for (int pageNum : mVisiblePages) {
- if (mPaginatedView.getViewAt(pageNum) == null) {
- mPageViewFactory.getOrCreatePageView(pageNum,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(pageNum),
- new PageTouchHandler());
- requiresLayoutPass = true;
- }
- }
+ boolean requiresLayoutPass = mPaginatedView.createPageViewsForVisiblePageRange();
// 4. Refresh tiles and/or full pages.
if (position.stable) {
// Perform a full refresh on all visible pages
- if (requiresLayoutPass) {
- refreshPagesAfterLayout(mVisiblePages);
- } else {
- refreshPages(mVisiblePages);
- }
- for (Range pages : gonePages) {
- clearPages(pages, true);
- }
- } else if (mStableZoom == position.zoom) {
+ mPaginatedView.refreshVisiblePages(requiresLayoutPass, viewState().get(),
+ mZoomView.getStableZoom());
+ mPaginatedView.handleGonePages(/* clearViews= */ true);
+ } else if (mZoomView.getStableZoom() == position.zoom) {
// Just load a few more tiles in case of tile-scroll
- if (requiresLayoutPass) {
- refreshTilesAfterLayout(mVisiblePages);
- } else {
- refreshTiles(mVisiblePages);
- }
+ mPaginatedView.refreshVisibleTiles(requiresLayoutPass, viewState().get());
}
- maybeLayoutPages(mVisiblePages.getLast());
- }
-
- private void refreshTiles(Range pages) {
- for (int page : pages) {
- PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
- page,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(page),
- new PageTouchHandler());
- pageView.requestTiles();
- }
- }
-
- private void refreshTilesAfterLayout(final Range pages) {
- ThreadUtils.postOnUiThread(
- () -> {
- if (viewState().get() != ViewState.NO_VIEW) {
- refreshTiles(pages);
- }
- });
- }
-
- private void loadPageOnly(Range pages) {
- for (int page : pages) {
- mPdfLoader.cancelAllTileBitmaps(page);
- PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
- page,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(page),
- new PageTouchHandler());
- pageView.clearTiles();
- pageView.requestFastDrawAtZoom(mStableZoom);
- loadVisiblePageText(page);
- maybeLoadFormAccessibilityInfo(page);
- }
- }
-
- private void refreshPages(Range pages) {
- for (int page : pages) {
- PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
- page,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(page),
- new PageTouchHandler());
- pageView.requestDrawAtZoom(mStableZoom);
- loadVisiblePageText(page);
- maybeLoadFormAccessibilityInfo(page);
- }
- }
-
- private void refreshPagesAfterLayout(final Range pages) {
- ThreadUtils.postOnUiThread(
- () -> {
- if (viewState().get() != ViewState.NO_VIEW) {
- refreshPages(pages);
- }
- });
- }
-
- private void clearPages(Range pages, boolean clearViews) {
- for (int page : pages) {
- // Don't cancel search - search results for the current search are always useful,
- // even for pages we can't see right now. Form filling operations should always
- // be executed against the document, even if the user has scrolled away from the page.
- mPdfLoader.cancelExceptSearchAndFormFilling(page);
- if (clearViews) {
- mPaginatedView.removeViewAt(page);
- }
- }
+ mLayoutHandler.maybeLayoutPages(
+ mPaginatedView.getPageRangeHandler().getVisiblePages().getLast());
}
/** Show the loading spinner. */
@@ -935,197 +677,13 @@
}
}
- private void loadVisiblePageText(int page) {
- PageView pageView = mPageViewFactory.getOrCreatePageView(
- page,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(page),
- new PageTouchHandler());
- PageMosaicView pageMosaicView = pageView.getPageView();
- if (pageMosaicView.needsPageText()) {
- mPdfLoader.loadPageText(page);
- }
- if (!pageMosaicView.hasPageUrlLinks()) {
- mPdfLoader.loadPageUrlLinks(page);
- }
- if (!pageMosaicView.hasPageGotoLinks()) {
- mPdfLoader.loadPageGotoLinks(page);
- }
- if (page == mSelectionModel.getPage()) {
- pageMosaicView.setOverlay(new PdfHighlightOverlay(mSelectionModel.selection().get()));
- } else if (mSearchModel.query().get() != null) {
- if (!pageMosaicView.hasOverlay()) {
- mPdfLoader.searchPageText(page, mSearchModel.query().get());
- }
- } else {
- pageMosaicView.setOverlay(null);
- }
- }
-
- /**
- * Load accessibility information for the form if document can be edited and accessibility is
- * required.
- */
- private void maybeLoadFormAccessibilityInfo(int pageNum) {
- mPageViewFactory.getOrCreatePageView(
- pageNum,
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(pageNum),
- new PageTouchHandler());
- }
-
- /** Computes the range of visible pages in the given position. */
- private Range computeVisibleRange(ZoomScroll position) {
- int top = Math.round(position.scrollY / position.zoom);
- int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
- Range window = new Range(top, bottom);
- return mPaginationModel.getPagesInWindow(window, true);
- }
-
- /** Expand the range to include more page(s) in the each direction. */
- private static Range expandRange(Range range, int margin, Range allPages) {
- return range.expand(margin, allPages);
- }
-
- /**
- * Computes the range of pages that are entirely visible, or if no page is entirely visible,
- * returns the most visible page.
- */
- private Range computeImportantRange(ZoomScroll position) {
- int top = Math.round(position.scrollY / position.zoom);
- int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
- Range window = new Range(top, bottom);
- return mPaginationModel.getPagesInWindow(window, false);
- }
-
- { // Listen to ZoomView.
- mZoomScrollObserver =
- new ValueObserver<ZoomScroll>() {
- @Override
- public void onChange(ZoomScroll oldPosition, ZoomScroll position) {
- loadPageAssets(position);
- if (mPageIndicator.setRangeAndZoom(
- computeImportantRange(position), position.zoom, position.stable)) {
- showFastScrollView();
- }
-
- if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
- if (position.scrollY > 0) {
- mAnnotationButton.setVisibility(View.GONE);
- } else if (position.scrollY == 0
- && mAnnotationButton.getVisibility() == View.GONE
- && mFindInFileView.getVisibility() == View.GONE) {
- mAnnotationButton.setVisibility(View.VISIBLE);
- }
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return TAG + "#zoomScrollObserver";
- }
- };
- }
-
- { // Listen to searchModel.
- mSearchQueryObserver =
- new ValueObserver<String>() {
- @Override
- public void onChange(String oldQuery, String newQuery) {
- mPaginatedView.clearAllOverlays();
- }
-
- @NonNull
- @Override
- public String toString() {
- return TAG + "#searchQueryObserver";
- }
- };
-
- mSelectedMatchObserver =
- new ValueObserver<SelectedMatch>() {
- @Override
- public void onChange(
- @Nullable SelectedMatch oldSelection,
- @Nullable SelectedMatch newSelection) {
- if (newSelection == null) {
- mPaginatedView.clearAllOverlays();
- return;
- }
- if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
- // Selected match has moved onto a new page - update the overlay on
- // the old page.
- getPage(oldSelection.getPage())
- .getPageView()
- .setOverlay(
- new PdfHighlightOverlay(oldSelection.getPageMatches()));
- }
- lookAtSelection(newSelection);
- }
-
- @NonNull
- @Override
- public String toString() {
- return TAG + "#selectedMatchObserver";
- }
- };
- }
-
- { // Listen to selectionModel.
- mSelectionObserver =
- new ValueObserver<PageSelection>() {
- @Override
- public void onChange(PageSelection oldSelection, PageSelection newSelection) {
- if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
- getPage(oldSelection.getPage()).getPageView().setOverlay(null);
- }
- if (newSelection != null && mVisiblePages.contains(
- newSelection.getPage())) {
- ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
- newSelection.getPage(),
- sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(newSelection.getPage()),
- new PageTouchHandler()))
- .setOverlay(new PdfHighlightOverlay(newSelection));
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return TAG + "#selectionObserver";
- }
- };
- }
-
- {
- mFastscrollerPositionObserver =
- new ValueObserver<Integer>() {
- @Override
- public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
- if (mPageIndicator != null && newValue != null) {
- mPageIndicator.getView().setY(
- newValue - (mPageIndicator.getView().getHeight() / 2));
- mPageIndicator.show();
- showFastScrollView();
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return TAG + "#fastscrollerPositionObserver";
- }
- };
- }
-
private FindInFileListener makeFindInFileListener() {
return new FindInFileListener() {
@Override
public boolean onQueryTextChange(@Nullable String query) {
if (mSearchModel != null) {
- mSearchModel.setQuery(query, getViewingPage());
+ mSearchModel.setQuery(query,
+ mPaginatedView.getPageRangeHandler().getVisiblePage());
return true;
}
return false;
@@ -1142,7 +700,8 @@
direction = CycleRange.Direction.FORWARDS;
// TODO: Track "find next" action event.
}
- mSearchModel.selectNextMatch(direction, getViewingPage());
+ mSearchModel.selectNextMatch(direction,
+ mPaginatedView.getPageRangeHandler().getVisiblePage());
return true;
}
return false;
@@ -1156,113 +715,6 @@
};
}
- public class PageTouchHandler {
- /**
- * Handles a tap event for non-formfilling actions.
- *
- * <p>This is includes comments, links and full screen toggle. Separate from form filling as
- * form filling involves asynchronous evaluations that must be completed outside normal
- * branch
- * statements.
- */
- public boolean handleSingleTapNoFormFilling(@NonNull MotionEvent event,
- @NonNull PageMosaicView pageMosaicView) {
- if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
- if (mAnnotationButton.getVisibility() == View.GONE
- && mFindInFileView.getVisibility() == GONE) {
- mAnnotationButton.setVisibility(View.VISIBLE);
- } else {
- mAnnotationButton.setVisibility(View.GONE);
- }
- }
- boolean hadSelection =
- mSelectionModel != null && mSelectionModel.selection().get() != null;
- if (hadSelection) {
- mSelectionModel.setSelection(null);
- }
-
- Point point = new Point((int) event.getX(), (int) event.getY());
- String linkUrl = pageMosaicView.getLinkUrl(point);
- if (linkUrl != null) {
- ExternalLinks.open(linkUrl, requireActivity());
- }
-
- GotoLinkDestination gotoDest = pageMosaicView.getGotoDestination(point);
- if (gotoDest != null) {
- gotoPageDest(gotoDest);
- }
-
- return true;
- }
-
- /** */
- public void gotoPageDest(@NonNull GotoLinkDestination destination) {
-
- if (destination.getPageNumber() >= mPaginationModel.getSize()) {
- // We have not yet loaded our destination.
- layoutPages(destination.getPageNumber() + 1);
- mDimensCallbackQueue.add(
- pageNum -> {
- if (pageNum == destination.getPageNumber()) {
- gotoPageDest(destination);
- return false;
- }
- return true;
- });
- return;
- }
-
- if (destination.getYCoordinate() != null) {
- int pageY = (int) destination.getYCoordinate().floatValue();
-
- Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber());
- int x = pageRect.left + (pageRect.width() / 2);
- int y = mPaginationModel.getLookAtY(destination.getPageNumber(), pageY);
- // Zoom should match the width of the page.
- float zoom =
- ZoomUtils.calculateZoomToFit(
- mZoomView.getViewportWidth(), mZoomView.getViewportHeight(),
- pageRect.width(), 1);
-
- mZoomView.setZoom(zoom);
- mZoomView.centerAt(x, y);
- } else {
- gotoPage(destination.getPageNumber());
- }
- }
-
- /** Goes to the {@code pageNum} and fits the page to the current viewport. */
- private void gotoPage(int pageNum) {
- if (pageNum >= mPaginationModel.getSize()) {
- // We have not yet loaded our destination.
- layoutPages(pageNum + 1);
- mDimensCallbackQueue.add(
- loadedPageNum -> {
- if (pageNum == loadedPageNum) {
- gotoPage(pageNum);
- return false;
- }
- return true;
- });
- return;
- }
-
- Rect pageRect = mPaginationModel.getPageLocation(pageNum);
-
- int x = pageRect.left + (pageRect.width() / 2);
- int y = pageRect.top + (pageRect.height() / 2);
- float zoom =
- ZoomUtils.calculateZoomToFit(
- mZoomView.getViewportWidth(),
- mZoomView.getViewportHeight(),
- pageRect.width(),
- pageRect.height());
-
- mZoomView.setZoom(zoom);
- mZoomView.centerAt(x, y);
- }
- }
-
// TODO: Revisit this method for its usage. Currently redundant
{ // Init pdfLoaderCallbacks
@@ -1329,19 +781,17 @@
}
mDocumentLoaded = true;
- PdfViewer.this.mNumPages = numPages;
-
hideSpinner();
// Assume we see at least the first page
- mMaxPage = 1;
+ mPaginatedView.getPageRangeHandler().setMaxPage(1);
if (viewState().get() != ViewState.NO_VIEW) {
mPaginationModel.initialize(numPages);
mPaginatedView.setModel(mPaginationModel);
mPaginationModel.addObserver(mPaginatedView);
dismissPasswordDialog();
- maybeLayoutPages(1);
+ mLayoutHandler.maybeLayoutPages(1);
mPageIndicator.setNumPages(numPages);
mSearchModel.setNumPages(numPages);
}
@@ -1350,7 +800,7 @@
mShouldRedrawOnDocumentLoaded = false;
}
- if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
+ if (mIsAnnotationIntentResolvable) {
mAnnotationButton.setVisibility(VISIBLE);
}
}
@@ -1392,8 +842,7 @@
((PageMosaicView) mPageViewFactory.getOrCreatePageView(
page,
sScreen.pxFromDp(PAGE_ELEVATION_DP),
- mPaginationModel.getPageSize(page),
- new PageTouchHandler()))
+ mPaginationModel.getPageSize(page)))
.setFailure(getString(R.string.error_on_page, page + 1));
Toaster.LONG.popToast(getActivity(), R.string.error_on_page, page + 1);
// TODO: Track render error.
@@ -1413,7 +862,7 @@
public void setPageDimensions(int pageNum, @NonNull Dimensions dimensions) {
if (viewState().get() != ViewState.NO_VIEW) {
mPaginationModel.addPage(pageNum, dimensions);
- mPageLayoutReach = mPaginationModel.getSize();
+ mLayoutHandler.setPageLayoutReach(mPaginationModel.getSize());
if (mSearchModel.query().get() != null
&& mSearchModel.selectedMatch().get() != null
@@ -1427,35 +876,24 @@
});
}
- ThreadUtils.postOnUiThread(
- () -> {
- if (mDimensCallbackQueue.isEmpty()
- || viewState().get() == ViewState.NO_VIEW) {
- return;
- }
-
- Iterator<OnDimensCallback> iterator =
- mDimensCallbackQueue.iterator();
- while (iterator.hasNext()) {
- OnDimensCallback callback = iterator.next();
- boolean shouldKeep = callback.onDimensLoaded(pageNum);
- if (!shouldKeep) {
- iterator.remove();
- }
- }
- });
+ mLayoutHandler.processCallbacksInQueue(viewState().get(), pageNum);
// The new page might actually be visible on the screen, so we need
// to fetch assets:
- Range newRange = computeVisibleRange(mZoomView.zoomScroll().get());
+ ZoomScroll position = mZoomView.zoomScroll().get();
+ Range newRange =
+ mPaginatedView.getPageRangeHandler().computeVisibleRange(
+ position.scrollY, position.zoom, mZoomView.getHeight(),
+ true);
if (newRange.isEmpty()) {
// During fast-scroll, we mostly don't need to fetch assets, but
// make sure we keep pushing layout bounds far enough, and update
// page numbers as we "scroll" down.
- if (mPageIndicator.setRangeAndZoom(newRange, mStableZoom, false)) {
+ if (mPageIndicator.setRangeAndZoom(newRange,
+ mZoomView.getStableZoom(), false)) {
showFastScrollView();
}
- maybeLayoutPages(newRange.getLast());
+ mLayoutHandler.maybeLayoutPages(newRange.getLast());
} else if (newRange.contains(pageNum)) {
// The new page is visible, fetch its assets.
loadPageAssets(mZoomView.zoomScroll().get());
@@ -1512,10 +950,6 @@
return;
}
if (selection != null) {
- if (mWaitingOnSelectionToCreateInlineComment) {
- mSelectionModel.setSelection(selection);
- return;
- }
// Clear searchModel - we hide the search and show the selection
// instead.
mSearchModel.setQuery(null, -1);
@@ -1665,7 +1099,7 @@
queryBox.clearFocus();
queryBox.setText("");
parentLayout.setVisibility(GONE);
- if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
+ if (mIsAnnotationIntentResolvable) {
mAnnotationButton.setVisibility(VISIBLE);
}
});
@@ -1687,4 +1121,5 @@
intent.setData(mLocalUri);
startActivity(intent);
}
+
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
similarity index 60%
copy from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
copy to pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
index 5138d2d..081e10d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
@@ -14,27 +14,21 @@
* limitations under the License.
*/
-package androidx.pdf.pdflib;
+package androidx.pdf.viewer;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.pdf.models.PdfDocumentRemote;
+import androidx.pdf.util.ObservableValue;
-/**
- *
- */
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentRemoteProto {
- private PdfDocumentRemote mRemote;
+class SearchQueryObserver implements ObservableValue.ValueObserver<String> {
+ private final PaginatedView mPaginatedView;
- public PdfDocumentRemoteProto(@NonNull PdfDocumentRemote remote) {
- this.mRemote = remote;
+ SearchQueryObserver(PaginatedView paginatedView) {
+ mPaginatedView = paginatedView;
}
- @NonNull
- public PdfDocumentRemote getPdfDocumentRemote() {
- return mRemote;
+ @Override
+ public void onChange(String oldQuery, String newQuery) {
+ mPaginatedView.clearAllOverlays();
}
-
- // TODO: Add goto links methods from the original kotlin file
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
new file mode 100644
index 0000000..734a15e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.PaginationUtils;
+import androidx.pdf.widget.ZoomView;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SelectedMatchValueObserver implements ObservableValue.ValueObserver<SelectedMatch> {
+ private final PaginatedView mPaginatedView;
+ private final PaginationModel mPaginationModel;
+ private final PageViewFactory mPageViewFactory;
+ private final LayoutHandler mLayoutHandler;
+ private final ZoomView mZoomView;
+ private final Context mContext;
+
+ SelectedMatchValueObserver(PaginatedView paginatedView, PaginationModel paginationModel,
+ PageViewFactory pageViewFactory, ZoomView zoomView, LayoutHandler layoutHandler,
+ Context context) {
+ mPaginatedView = paginatedView;
+ mPaginationModel = paginationModel;
+ mPageViewFactory = pageViewFactory;
+ mZoomView = zoomView;
+ mLayoutHandler = layoutHandler;
+ mContext = context;
+ }
+
+ @Override
+ public void onChange(
+ @Nullable SelectedMatch oldSelection,
+ @Nullable SelectedMatch newSelection) {
+ if (newSelection == null) {
+ mPaginatedView.clearAllOverlays();
+ return;
+ }
+ if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+ // Selected match has moved onto a new page - update the overlay on
+ // the old page.
+ mPaginatedView.getViewAt(oldSelection.getPage())
+ .getPageView()
+ .setOverlay(
+ new PdfHighlightOverlay(oldSelection.getPageMatches()));
+ }
+ lookAtSelection(newSelection);
+ }
+
+ private boolean isPageCreated(int pageNum) {
+ return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ }
+
+ private void lookAtSelection(SelectedMatch selection) {
+ if (selection == null || selection.isEmpty()) {
+ return;
+ }
+ if (selection.getPage() >= mPaginationModel.getSize()) {
+ mLayoutHandler.layoutPages(selection.getPage() + 1);
+ return;
+ }
+ Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
+ int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
+ int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
+ mZoomView.centerAt(x, y);
+
+ PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+ selection.getPage(),
+ PaginationUtils.getPageElevationInPixels(mContext),
+ mPaginationModel.getPageSize(selection.getPage()));
+ pageView.setOverlay(selection.getOverlay());
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
new file mode 100644
index 0000000..74f02a6
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static android.view.View.GONE;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.models.GotoLinkDestination;
+import androidx.pdf.util.ExternalLinks;
+import androidx.pdf.util.ZoomUtils;
+import androidx.pdf.widget.ZoomView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SingleTapHandler {
+ private final Context mContext;
+ private final FloatingActionButton mFloatingActionButton;
+ private final FindInFileView mFindInFileView;
+ private final ZoomView mZoomView;
+ private final PdfSelectionModel mPdfSelectionModel;
+ private final PaginationModel mPaginationModel;
+ private final LayoutHandler mLayoutHandler;
+ private boolean mIsAnnotationIntentResolvable;
+
+ public SingleTapHandler(@NonNull Context context,
+ @NonNull FloatingActionButton floatingActionButton,
+ @NonNull FindInFileView findInFileView,
+ @NonNull ZoomView zoomView,
+ @NonNull PdfSelectionModel pdfSelectionModel,
+ @NonNull PaginationModel paginationModel,
+ @NonNull LayoutHandler layoutHandler) {
+ mContext = context;
+ mFloatingActionButton = floatingActionButton;
+ mFindInFileView = findInFileView;
+ mZoomView = zoomView;
+ mPdfSelectionModel = pdfSelectionModel;
+ mPaginationModel = paginationModel;
+ mLayoutHandler = layoutHandler;
+ }
+
+ public void setAnnotationIntentResolvable(boolean annotationIntentResolvable) {
+ mIsAnnotationIntentResolvable = annotationIntentResolvable;
+ }
+
+ /** */
+ public void handleSingleTapConfirmedEventOnPage(@NonNull MotionEvent event,
+ @NonNull PageMosaicView pageMosaicView) {
+ if (mIsAnnotationIntentResolvable) {
+ if (mFloatingActionButton.getVisibility() == View.GONE
+ && mFindInFileView.getVisibility() == GONE) {
+ mFloatingActionButton.setVisibility(View.VISIBLE);
+ } else {
+ mFloatingActionButton.setVisibility(View.GONE);
+ }
+ }
+
+ handleSelection();
+
+ Point point = new Point((int) event.getX(), (int) event.getY());
+ handleExternalLink(point, pageMosaicView);
+
+ GotoLinkDestination gotoDest = pageMosaicView.getGotoDestination(point);
+ if (gotoDest != null) {
+ gotoPageDest(gotoDest);
+ }
+ }
+
+ private void handleSelection() {
+ boolean hadSelection =
+ mPdfSelectionModel != null && mPdfSelectionModel.selection().get() != null;
+ if (hadSelection) {
+ mPdfSelectionModel.setSelection(null);
+ }
+ }
+
+ private void handleExternalLink(Point point, PageMosaicView pageMosaicView) {
+ String linkUrl = pageMosaicView.getLinkUrl(point);
+ if (linkUrl != null) {
+ ExternalLinks.open(linkUrl, mContext);
+ }
+ }
+
+ /** */
+ public void gotoPageDest(@NonNull GotoLinkDestination destination) {
+
+ if (destination.getPageNumber() >= mPaginationModel.getSize()) {
+ // We have not yet loaded our destination.
+ mLayoutHandler.layoutPages(destination.getPageNumber() + 1);
+ mLayoutHandler.add(
+ pageNum -> {
+ if (pageNum == destination.getPageNumber()) {
+ gotoPageDest(destination);
+ return false;
+ }
+ return true;
+ });
+ return;
+ }
+
+ if (destination.getYCoordinate() != null) {
+ int pageY = (int) destination.getYCoordinate().floatValue();
+
+ Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber());
+ int x = pageRect.left + (pageRect.width() / 2);
+ int y = mPaginationModel.getLookAtY(destination.getPageNumber(), pageY);
+ // Zoom should match the width of the page.
+ float zoom =
+ ZoomUtils.calculateZoomToFit(
+ mZoomView.getViewportWidth(), mZoomView.getViewportHeight(),
+ pageRect.width(), 1);
+
+ mZoomView.setZoom(zoom);
+ mZoomView.centerAt(x, y);
+ } else {
+ gotoPage(destination.getPageNumber());
+ }
+ }
+
+ /** Goes to the {@code pageNum} and fits the page to the current viewport. */
+ private void gotoPage(int pageNum) {
+ if (pageNum >= mPaginationModel.getSize()) {
+ // We have not yet loaded our destination.
+ mLayoutHandler.layoutPages(pageNum + 1);
+ mLayoutHandler.add(
+ loadedPageNum -> {
+ if (pageNum == loadedPageNum) {
+ gotoPage(pageNum);
+ return false;
+ }
+ return true;
+ });
+ return;
+ }
+
+ Rect pageRect = mPaginationModel.getPageLocation(pageNum);
+
+ int x = pageRect.left + (pageRect.width() / 2);
+ int y = pageRect.top + (pageRect.height() / 2);
+ float zoom =
+ ZoomUtils.calculateZoomToFit(
+ mZoomView.getViewportWidth(),
+ mZoomView.getViewportHeight(),
+ pageRect.width(),
+ pageRect.height());
+
+ mZoomView.setZoom(zoom);
+ mZoomView.centerAt(x, y);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
index 64a949d..b81b180 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
@@ -56,14 +56,8 @@
@SuppressWarnings("deprecation")
public abstract class Viewer extends Fragment {
- @NonNull
- protected abstract String getLogTag();
-
protected static final String KEY_DATA = "data";
- /** Scale for the progress metric. */
- protected static final int PROGRESS_SCALER = 100;
-
/**
* The state of the view hierarchy for this {@link Fragment}, as exposed by {@link #mViewState}.
*/
@@ -121,9 +115,6 @@
protected ExposedValue<ViewState> mViewState = Observables.newExposedValueWithInitialValue(
ViewState.NO_VIEW);
- // Debug log of lifecycle events that happened on this viewer, helps investigating.
- private final StringBuilder mEventlog = new StringBuilder();
-
{
// We can call getArguments() from setters and know that it will not be null.
setArguments(new Bundle());
@@ -203,13 +194,6 @@
}
}
- /** Notifies this Viewer goes off-screen. {@link #onExit()} will be called immediately. */
- public void exit() {
- mDelayedEnter = false; // in case we never started.
- onExit();
- mOnScreen = false;
- }
-
/** Called after this viewer enters the screen and becomes visible. */
@CallSuper
protected void onEnter() {
@@ -314,18 +298,6 @@
getArguments().putBundle(KEY_DATA, data.asBundle());
}
- /** Returns a compact event log for this Viewer that helps investigating lifecycle issues. */
- @NonNull
- protected String getEventlog() {
- return mEventlog.toString();
- }
-
- /** Returns the length of the current file. The meaning of the length is type dependent. */
- public abstract long getContentLength();
-
- /** Returns the user's current progress in the file in percentage. */
- public abstract int getViewProgress();
-
@Override
protected void finalize() throws Throwable {
super.finalize();
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
new file mode 100644
index 0000000..90bb371
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import android.view.View;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.widget.FastScrollView;
+import androidx.pdf.widget.ZoomView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+class ZoomScrollValueObserver implements ObservableValue.ValueObserver<ZoomView.ZoomScroll> {
+ private final PaginatedView mPaginatedView;
+ private final ZoomView mZoomView;
+ private final LayoutHandler mLayoutHandler;
+ private final FloatingActionButton mAnnotationButton;
+ private final FindInFileView mFindInFileView;
+ private final PageIndicator mPageIndicator;
+ private final FastScrollView mFastScrollView;
+ private final boolean mIsAnnotationIntentResolvable;
+ private final ObservableValue<Viewer.ViewState> mViewState;
+
+ ZoomScrollValueObserver(ZoomView zoomView, PaginatedView paginatedView,
+ LayoutHandler layoutHandler,
+ FloatingActionButton annotationButton, FindInFileView findInFileView,
+ PageIndicator pageIndicator, FastScrollView fastScrollView,
+ boolean isAnnotationIntentResolvable, ObservableValue<Viewer.ViewState> viewState) {
+ mZoomView = zoomView;
+ mPaginatedView = paginatedView;
+ mLayoutHandler = layoutHandler;
+ mAnnotationButton = annotationButton;
+ mFindInFileView = findInFileView;
+ mPageIndicator = pageIndicator;
+ mFastScrollView = fastScrollView;
+ mIsAnnotationIntentResolvable = isAnnotationIntentResolvable;
+ mViewState = viewState;
+ }
+
+ @Override
+ public void onChange(ZoomView.ZoomScroll oldPosition, ZoomView.ZoomScroll position) {
+ loadPageAssets(position);
+ Range visiblePageRange = mPaginatedView.getPageRangeHandler()
+ .computeVisibleRange(position.scrollY, position.zoom,
+ mZoomView.getHeight(), false);
+ if (mPageIndicator.setRangeAndZoom(visiblePageRange, position.zoom,
+ position.stable)) {
+ showFastScrollView();
+ }
+
+ if (mIsAnnotationIntentResolvable) {
+ if (position.scrollY > 0) {
+ mAnnotationButton.setVisibility(View.GONE);
+ } else if (position.scrollY == 0
+ && mAnnotationButton.getVisibility() == View.GONE
+ && mFindInFileView.getVisibility() == View.GONE) {
+ mAnnotationButton.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ private void loadPageAssets(ZoomView.ZoomScroll position) {
+ // Change the resolution of the bitmaps only when a gesture is not in progress.
+ if (position.stable || mZoomView.getStableZoom() == 0) {
+ mZoomView.setStableZoom(position.zoom);
+ }
+
+ mPaginatedView.getPaginationModel().setViewArea(mZoomView.getVisibleAreaInContentCoords());
+ mPaginatedView.refreshPageRangeInVisibleArea(position, mZoomView.getHeight());
+ mPaginatedView.handleGonePages(/* clearViews= */ false);
+ mPaginatedView.loadInvisibleNearPageRange(mZoomView.getStableZoom());
+
+ // The step (4) below requires page Views to be created and laid out. So we create them here
+ // and set this flag if that operation needs to wait for a layout pass.
+ boolean requiresLayoutPass = mPaginatedView.createPageViewsForVisiblePageRange();
+
+ // 4. Refresh tiles and/or full pages.
+ if (position.stable) {
+ // Perform a full refresh on all visible pages
+ mPaginatedView.refreshVisiblePages(requiresLayoutPass, mViewState.get(),
+ mZoomView.getStableZoom());
+ mPaginatedView.handleGonePages(/* clearViews= */ true);
+ } else if (mZoomView.getStableZoom() == position.zoom) {
+ // Just load a few more tiles in case of tile-scroll
+ mPaginatedView.refreshVisibleTiles(requiresLayoutPass, mViewState.get());
+ }
+
+ if (mPaginatedView.getPageRangeHandler().getVisiblePages() != null) {
+ mLayoutHandler.maybeLayoutPages(
+ mPaginatedView.getPageRangeHandler().getVisiblePages().getLast());
+ }
+ }
+
+ private void showFastScrollView() {
+ if (mFastScrollView != null) {
+ mFastScrollView.setVisible();
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
index fafea44..7da2a94 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
@@ -21,7 +21,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
import androidx.pdf.util.ThreadUtils;
/**
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
index ce69c0cb..7c1e5d7 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
@@ -20,7 +20,7 @@
import android.os.RemoteException;
import androidx.annotation.RestrictTo;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
index 5758c6a..df72681 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
@@ -26,7 +26,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentService;
+import androidx.pdf.service.PdfDocumentService;
import androidx.pdf.util.Preconditions;
import java.util.concurrent.locks.Condition;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
index 2884cd6..933f7a8 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
@@ -32,7 +32,7 @@
import androidx.pdf.models.Dimensions;
import androidx.pdf.models.PdfDocumentRemote;
import androidx.pdf.models.SelectionBoundary;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
import androidx.pdf.util.BitmapRecycler;
import androidx.pdf.util.TileBoard.TileInfo;
@@ -67,6 +67,7 @@
private final SparseArray<PdfPageLoader> mPageLoaders;
private String mLoadedPassword;
+ private int mNumPages;
/** Creates a new {@link PdfLoader} and loads the document from the given data. */
@NonNull
@@ -126,6 +127,14 @@
this.mPageLoaders = new SparseArray<>();
}
+ public int getNumPages() {
+ return mNumPages;
+ }
+
+ public void setNumPages(int numPages) {
+ mNumPages = numPages;
+ }
+
/** Schedule task to load a PdfDocument. */
public void reloadDocument() {
mExecutor.schedule(new LoadDocumentTask(mLoadedPassword));
@@ -347,7 +356,6 @@
/** AsyncTask for loading a PdfDocument. */
class LoadDocumentTask extends AbstractPdfTask<PdfStatus> {
private final String mPassword;
- private int mNumPages;
// private boolean mIsLinearized;
LoadDocumentTask() {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
index 61cddc6..d446bf0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
@@ -30,9 +30,7 @@
import androidx.pdf.models.MatchRects;
import androidx.pdf.models.PageSelection;
import androidx.pdf.models.SelectionBoundary;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
-import androidx.pdf.util.BitmapParcel;
-import androidx.pdf.util.StrictModeUtils;
+import androidx.pdf.service.PdfDocumentRemoteProto;
import androidx.pdf.util.TileBoard.TileInfo;
import com.google.common.collect.ImmutableList;
@@ -58,14 +56,6 @@
/** Arbitrary dimensions used for pages that are broken. */
private static final Dimensions DEFAULT_PAGE = new Dimensions(400, 400);
- static {
- // TODO: StrictMode- disk read 14ms.
- // NOTE: this line can break when running with --noforge, such as when debugging with
- // Android Studio. You may need to comment it out locally if you see errors like
- // `java.lang.UnsatisfiedLinkError: no bitmap_parcel in java.library.path`.
- StrictModeUtils.bypass(() -> BitmapParcel.loadNdkLib());
- }
-
private final PdfLoader mParent;
private final int mPageNum;
private final boolean mHideTextAnnotations;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
index faa86b3..178d2b6 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
@@ -54,6 +54,8 @@
import androidx.pdf.util.Screen;
import androidx.pdf.util.ThreadUtils;
import androidx.pdf.util.ZoomScrollRestorer;
+import androidx.pdf.util.ZoomUtils;
+import androidx.pdf.viewer.PdfSelectionModel;
import com.google.android.material.motion.MotionUtils;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -196,6 +198,9 @@
private int mContentResizedModeZoom = ContentResizedMode.KEEP_SAME_ABSOLUTE;
private boolean mOverrideMinZoomToFit = false;
private boolean mOverrideMaxZoomToFit = false;
+
+ /** The last stable zoom: we only re-draw bitmaps at stable zoom (not during a gesture). */
+ private float mStableZoom;
/**
* If set to true, suppresses {@link ZoomGestureHandler#onScale(ScaleGestureDetector)} behavior.
*/
@@ -211,6 +216,11 @@
*/
private Rect mPaddingOnLastViewportUpdate;
+ /** Base padding for ZoomView in px as set in saveZoomViewBasePadding(). */
+ private Rect mZoomViewBasePadding = new Rect();
+ private boolean mZoomViewBasePaddingSaved;
+ private PdfSelectionModel mPdfSelectionModel;
+
{
mScroller = new RelativeScroller(getContext());
mPosition = Observables.newExposedValueWithInitialValue(new ZoomScroll(1, 0, 0, STABLE));
@@ -234,51 +244,6 @@
ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR);
}
- private static int scrollDeltaNeededForZoomChange(
- float oldZoom, float newZoom, float zoomviewPivot, int scroll) {
- // Find where the given pivot point would move to when we change the zoom, and return the
- // delta.
- float contentPivot = toContentCoord(zoomviewPivot, oldZoom, scroll);
- float movedZoomViewPivot = toZoomViewCoord(contentPivot, newZoom, scroll);
- return (int) (movedZoomViewPivot - zoomviewPivot);
- }
-
- private static float toContentCoord(float zoomViewCoord, float zoom, int scroll) {
- return (zoomViewCoord + scroll) / zoom;
- }
-
- private static float toZoomViewCoord(float contentCoord, float zoom, int scroll) {
- return (contentCoord * zoom) - scroll;
- }
-
- private static int constrain(float zoom, int scroll, int contentRawSize, int viewportSize) {
- // The variables in this method are named left and right, which is accurate when this
- // method is used to constrain X position - when constraining Y, left means top and right
- // means bottom.
-
- // Find the left and right bounds of the content in the zoomview's co-ordinates.
- float leftBound = toZoomViewCoord(0, zoom, scroll);
- float rightBound = toZoomViewCoord(contentRawSize, zoom, scroll);
-
- if (leftBound <= 0 && rightBound >= viewportSize) {
- // Content too large for viewport and no dead margins: no adjustment needed.
- return 0;
- }
- float scaledContentSize = rightBound - leftBound;
- if (scaledContentSize <= viewportSize) {
- // Content fits in viewport: keep in the center.
- return (int) ((rightBound + leftBound - viewportSize) / 2);
- } else {
- // Content doesn't fit in viewport: eliminate dead margins.
- if (leftBound > 0) { // Dead margin on the left.
- return (int) leftBound;
- } else if (rightBound < viewportSize) { // Dead margin on the right.
- return (int) (rightBound - viewportSize);
- }
- }
- return 0;
- }
-
/**
* Set values of shareScrollToLeft, shareScrollToRight, shareScrollToTop and
* shareScrollToBottom.
@@ -412,6 +377,15 @@
return this;
}
+ @Nullable
+ public PdfSelectionModel getPdfSelectionModel() {
+ return mPdfSelectionModel;
+ }
+
+ public void setPdfSelectionModel(@NonNull PdfSelectionModel pdfSelectionModel) {
+ mPdfSelectionModel = pdfSelectionModel;
+ }
+
/** Exposes this view's position as an observable value. */
@NonNull
public ObservableValue<ZoomScroll> zoomScroll() {
@@ -658,6 +632,13 @@
this.mMaxZoom = maxZoom;
}
+ public float getStableZoom() {
+ return mStableZoom;
+ }
+
+ public void setStableZoom(float stableZoom) {
+ this.mStableZoom = stableZoom;
+ }
private float getConstrainedZoomToFit() {
return constrainZoom(getUnconstrainedZoomToFit());
}
@@ -764,8 +745,10 @@
}
float left = x * newZoom - mViewport.width() / 2f;
float top = y * newZoom - mViewport.height() / 2f;
- left += constrain(newZoom, (int) left, mContentRawBounds.width(), mViewport.width());
- top += constrain(newZoom, (int) top, mContentRawBounds.height(), mViewport.height());
+ left += ZoomUtils.constrainCoordinate(newZoom, (int) left, mContentRawBounds.width(),
+ mViewport.width());
+ top += ZoomUtils.constrainCoordinate(newZoom, (int) top, mContentRawBounds.height(),
+ mViewport.height());
zoomScrollAnimated(left, top, newZoom, updateListener);
}
@@ -853,24 +836,51 @@
public void setZoom(float zoom, float pivotX, float pivotY) {
zoom = Float.isNaN(zoom) ? ZOOM_RESET : zoom;
mInitialZoomDone = true;
- int deltaX = scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotX, getScrollX());
- int deltaY = scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotY, getScrollY());
+ int deltaX = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotX,
+ getScrollX());
+ int deltaY = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotY,
+ getScrollY());
mContentView.setScaleX(zoom);
mContentView.setScaleY(zoom);
scrollBy(deltaX, deltaY);
}
+ @NonNull
+ public Rect getZoomViewBasePadding() {
+ return mZoomViewBasePadding;
+ }
+
+ public void setZoomViewBasePadding(@NonNull Rect zoomViewBasePadding) {
+ mZoomViewBasePadding = zoomViewBasePadding;
+ }
+
+ public boolean isZoomViewBasePaddingSaved() {
+ return mZoomViewBasePaddingSaved;
+ }
+
+ public void setZoomViewBasePaddingSaved(boolean zoomViewBasePaddingSaved) {
+ mZoomViewBasePaddingSaved = zoomViewBasePaddingSaved;
+ }
+
+ /** Invokes the [View#setPadding(int, int, int, int)] by adding the base padding */
+ public void setPaddingWithBase(int left, int top, int right, int bottom) {
+ super.setPadding(/* left = */ mZoomViewBasePadding.left + left,
+ /* top = */ mZoomViewBasePadding.top + top,
+ /* right = */ mZoomViewBasePadding.right + right,
+ /* bottom = */ mZoomViewBasePadding.bottom + bottom);
+ }
+
/**
* Given a point in the zoom-view's co-ordinates, convert it to the content's co-ordinates,
* using the current zoom and scroll position of the zoomview.
*/
protected float toContentX(float zoomViewX) {
- return toContentCoord(zoomViewX, getZoom(), getScrollX());
+ return ZoomUtils.toContentCoordinate(zoomViewX, getZoom(), getScrollX());
}
protected float toContentY(float zoomViewY) {
- return toContentCoord(zoomViewY, getZoom(), getScrollY());
+ return ZoomUtils.toContentCoordinate(zoomViewY, getZoom(), getScrollY());
}
/**
@@ -878,11 +888,11 @@
* using the current zoom and scroll position of the zoomview.
*/
protected float toZoomViewX(float contentX) {
- return toZoomViewCoord(contentX, getZoom(), getScrollX());
+ return ZoomUtils.toZoomViewCoordinate(contentX, getZoom(), getScrollX());
}
protected float toZoomViewY(float contentY) {
- return toZoomViewCoord(contentY, getZoom(), getScrollY());
+ return ZoomUtils.toZoomViewCoordinate(contentY, getZoom(), getScrollY());
}
/**
@@ -915,11 +925,13 @@
}
private int constrainX() {
- return constrain(getZoom(), getScrollX(), mContentRawBounds.width(), mViewport.width());
+ return ZoomUtils.constrainCoordinate(getZoom(), getScrollX(), mContentRawBounds.width(),
+ mViewport.width());
}
private int constrainY() {
- return constrain(getZoom(), getScrollY(), mContentRawBounds.height(), mViewport.height());
+ return ZoomUtils.constrainCoordinate(getZoom(), getScrollY(), mContentRawBounds.height(),
+ mViewport.height());
}
/**
@@ -1055,6 +1067,58 @@
return new Rect(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
}
+ /**
+ * Saves the padding set on {@link ZoomView} following initial inflation from XML.
+ *
+ * <p>This does not have to be called immediately following inflation but <i>must</i> be called
+ * before any methods change the padding on {@link ZoomView}.
+ *
+ * <p>This can be used by methods that need to set padding to (base padding + some other
+ * dimension). If these values were obtained directly from {@link ZoomView} or this method was
+ * allowed to execute multiple times it could result in padding expanding continually.
+ */
+ public void saveZoomViewBasePadding() {
+ if (mZoomViewBasePaddingSaved) {
+ return;
+ }
+
+ mZoomViewBasePadding =
+ new Rect(
+ getPaddingLeft(),
+ getPaddingTop(),
+ getPaddingRight(),
+ getPaddingBottom());
+
+ mZoomViewBasePadding.top +=
+ getResources().getDimensionPixelSize(R.dimen.viewer_doc_additional_top_offset);
+
+ mZoomViewBasePaddingSaved = true;
+ }
+
+ /**
+ * Adjusts the horizontal margins (left and right padding) of the ZoomView based on the
+ * screen width to optimize the display of PDF content.
+ *
+ * This method applies different margin values depending on the screen size:
+ * - For screens with a screen width of 840dp or greater, a larger margin is applied
+ * to enhance readability on larger displays.
+ * - For screens with a screen width < 840dp, no margin is used to
+ * maximize the use of available space.
+ *
+ * This dynamic adjustment is achieved through the use of resource qualifiers (values-w840dp)
+ * that define different margin values for different screen sizes.
+ *
+ * Note: This method does not affect the top or bottom padding of the ZoomView.
+ */
+ public void adjustZoomViewMargins() {
+ int margin = getResources().getDimensionPixelSize(R.dimen.viewer_doc_padding_x);
+
+ setPadding(margin,
+ getPaddingTop(),
+ margin,
+ getPaddingBottom());
+ }
+
/** Different options for the initial zoom that this ZoomView should start with. */
public static final class InitialZoomMode {
/** Start with a constant initial zoom, as specified by {@link #setInitialZoom(float)}. */
@@ -1394,15 +1458,19 @@
int newScrollX = oldScrollX;
int newScrollY = oldScrollY;
// Adjust new scroll positions due to changed zoom.
- newScrollX += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e1.getX(),
+ newScrollX += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom,
+ e1.getX(),
oldScrollX);
- newScrollY += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e1.getY(),
+ newScrollY += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom,
+ e1.getY(),
oldScrollY);
// Constrain new scroll positions.
- newScrollX += constrain(newZoom, newScrollX, mContentRawBounds.width(),
+ newScrollX += ZoomUtils.constrainCoordinate(newZoom, newScrollX,
+ mContentRawBounds.width(),
mViewport.width());
- newScrollY += constrain(newZoom, newScrollY, mContentRawBounds.height(),
+ newScrollY += ZoomUtils.constrainCoordinate(newZoom, newScrollY,
+ mContentRawBounds.height(),
mViewport.height());
zoomScrollAnimated(
@@ -1478,15 +1546,17 @@
int newScrollX = oldScrollX;
int newScrollY = oldScrollY;
// Adjust new scroll positions due to changed zoom.
- newScrollX += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getX(),
+ newScrollX += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getX(),
oldScrollX);
- newScrollY += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getY(),
+ newScrollY += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getY(),
oldScrollY);
// Constrain new scroll positions.
- newScrollX += constrain(newZoom, newScrollX, mContentRawBounds.width(),
+ newScrollX += ZoomUtils.constrainCoordinate(newZoom, newScrollX,
+ mContentRawBounds.width(),
mViewport.width());
- newScrollY += constrain(newZoom, newScrollY, mContentRawBounds.height(),
+ newScrollY += ZoomUtils.constrainCoordinate(newZoom, newScrollY,
+ mContentRawBounds.height(),
mViewport.height());
zoomScrollAnimated(newScrollX, newScrollY, newZoom, null /* updateListener */);
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt
new file mode 100644
index 0000000..bf37790
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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.pdf.exceptions
+
+import androidx.annotation.RestrictTo
+import java.lang.SecurityException
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/**
+ * Represents the exception thrown when a password is required or an incorrect password is supplied
+ * to a protected document
+ */
+class PdfPasswordException(message: String) : SecurityException(message)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt
new file mode 100644
index 0000000..d39576e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 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.pdf.loader
+
+import android.graphics.Bitmap
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.pdf.content.PdfPageGotoLinkContent
+import android.graphics.pdf.content.PdfPageImageContent
+import android.graphics.pdf.content.PdfPageLinkContent
+import android.graphics.pdf.content.PdfPageTextContent
+import android.graphics.pdf.models.PageMatchBounds
+import android.graphics.pdf.models.selection.PageSelection
+import android.util.Size
+import android.util.SparseArray
+import androidx.annotation.RestrictTo
+import java.io.Closeable
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/** Represents a PDF document and provides methods to interact with its content. */
+interface PdfDocument : Closeable {
+
+ /** The total number of pages in the document. */
+ val pageCount: Int
+
+ /** Indicates whether the document is linearized (optimized for fast web viewing). */
+ val isLinearized: Boolean
+
+ /** The type of form present in the document. */
+ val formType: Int
+
+ /** Indicates whether the document is password-protected. */
+ val isProtected: Boolean
+
+ /**
+ * Asynchronously retrieves information about the specified page.
+ *
+ * @param pageNumber The page number (0-based).
+ * @return A [PageInfo] object containing information about the page.
+ */
+ suspend fun getPageInfo(pageNumber: Int): PageInfo
+
+ /**
+ * Asynchronously retrieves information about a range of pages.
+ *
+ * @param pageRange The range of page numbers (0-based, inclusive).
+ * @return A list of {@link PageInfo} objects, one for each page in the range.
+ */
+ suspend fun getPageInfos(pageRange: IntRange): List<PageInfo>
+
+ /**
+ * Asynchronously searches the document for the specified query within a range of pages.
+ *
+ * @param query The search query string.
+ * @param pageRange The range of page numbers (0-based, inclusive) to search within.
+ * @return A {@link SparseArray} mapping page numbers to lists of {@link PageMatchBounds}
+ * objects representing the search results on each page.
+ */
+ suspend fun searchDocument(
+ query: String,
+ pageRange: IntRange
+ ): SparseArray<List<PageMatchBounds>>
+
+ /**
+ * Asynchronously retrieves the selection bounds (in PDF coordinates) for the specified text
+ * selection.
+ *
+ * @param start The starting point of the text selection.
+ * @param stop The ending point of the text selection.
+ * @return A SparseArray mapping page numbers to {@link PageSelection} objects representing the
+ * selection bounds on each page.
+ */
+ suspend fun getSelectionBounds(start: PdfPoint, stop: PdfPoint): SparseArray<PageSelection>
+
+ /**
+ * Asynchronously retrieves the content (text and images) of the specified page.
+ *
+ * @param pageNumber The page number (0-based).
+ * @return A {@link PdfPageContent} object representing the page's content.
+ */
+ suspend fun getPageContent(pageNumber: Int): PdfPageContent
+
+ /**
+ * Asynchronously retrieves the links (Go To and external) present on the specified page.
+ *
+ * @param pageNumber The page number (0-based).
+ * @return A [PdfPageLinks] object representing the page's links.
+ */
+ suspend fun getPageLinks(pageNumber: Int): PdfPageLinks
+
+ /**
+ * Gets a [BitmapSource] for retrieving bitmap representations of the specified page.
+ *
+ * @param pageNumber The page number (0-based).
+ * @return A [BitmapSource] for the specified page, or null if the page number is invalid.
+ */
+ fun getPageBitmapSource(pageNumber: Int): BitmapSource
+
+ /**
+ * Represents information about a single page in the PDF document.
+ *
+ * @property pageNum The page number (0-based).
+ * @property height The height of the page in points.
+ * @property width The width of the page in points.
+ */
+ class PageInfo(val pageNum: Int, val height: Int, val width: Int)
+
+ /** A source for retrieving bitmap representations of PDF pages. */
+ interface BitmapSource : Closeable {
+ /**
+ * Asynchronously retrieves a bitmap representation of the page, optionally constrained to a
+ * specific tile region.
+ *
+ * @param scaledPageSizePx The scaled page size in pixels, representing the page size in
+ * case of no zoom, and scaled page size in case of zooming.
+ * @param tileRegion (Optional) The region of the page to include in the bitmap, in pixels
+ * within the `scaledPageSizePx`. This identifies the tile. If null, the entire page is
+ * included.
+ * @return The bitmap representation of the page.
+ */
+ suspend fun getBitmap(scaledPageSizePx: Size, tileRegion: Rect? = null): Bitmap
+ }
+
+ /**
+ * Represents the combined text and image content within a single page of a PDF document.
+ *
+ * @property textContents A list of {@link PdfPageTextContent} objects representing the text
+ * elements on the page.
+ * @property imageContents A list of {@link PdfPageImageContent} objects representing the image
+ * elements on the page.
+ */
+ class PdfPageContent(
+ val textContents: List<PdfPageTextContent>,
+ val imageContents: List<PdfPageImageContent>
+ )
+
+ /**
+ * Represents the links within a single page of a PDF document.
+ *
+ * @property gotoLinks A list of internal links (links to other pages within the same document)
+ * represented as `PdfPageGotoLinkContent` objects.
+ * @property externalLinks A list of external links (links to web pages or other resources)
+ * represented as `PdfPageLinkContent` objects.
+ */
+ class PdfPageLinks(
+ val gotoLinks: List<PdfPageGotoLinkContent>,
+ val externalLinks: List<PdfPageLinkContent>
+ )
+
+ /**
+ * Represents a point within a specific page of a PDF document.
+ *
+ * @property pageNumber The page number (0-based) where the point is located.
+ * @property pagePoint The coordinates (x, y) of the point relative to the page's origin.
+ */
+ class PdfPoint(val pageNumber: Int, val pagePoint: PointF)
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt
new file mode 100644
index 0000000..f8b8e776
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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.pdf.loader
+
+import android.net.Uri
+import androidx.annotation.RestrictTo
+import androidx.pdf.exceptions.PdfPasswordException
+import java.io.IOException
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/**
+ * Provides an abstraction for asynchronously opening PDF documents from a Uri. Implementations of
+ * this interface are responsible for handling the retrieval, decryption (if necessary), and
+ * creation of a [PdfDocument] representation.
+ */
+interface PdfLoader {
+
+ /**
+ * Asynchronously opens a PDF document from the specified [Uri].
+ *
+ * @param uri The URI of the PDF document to open.
+ * @param password (Optional) The password to unlock the document if it is encrypted.
+ * @return The opened [PdfDocument].
+ * @throws PdfPasswordException If the provided password is incorrect.
+ * @throws IOException If an error occurs while opening the document.
+ */
+ @Throws(PdfPasswordException::class, IOException::class)
+ suspend fun openDocument(uri: Uri, password: String? = null): PdfDocument
+}
diff --git a/pdf/pdf-viewer/src/main/native/CMakeLists.txt b/pdf/pdf-viewer/src/main/native/CMakeLists.txt
deleted file mode 100644
index e687666..0000000
--- a/pdf/pdf-viewer/src/main/native/CMakeLists.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright (C) 2024 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.
-
-# For more information about using CMake with Android Studio, read the
-# documentation: https://d.android.com/studio/projects/add-native-code.html.
-# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
-
-# Sets the minimum CMake version required for this project.
-cmake_minimum_required(VERSION 3.22.1)
-
-# Declares the project name. The project name can be accessed via ${PROJECT_NAME}.
-# Since this is the top level CMakeLists.txt, the project name is also accessible
-# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
-# build script scope).
-project("bitmapParcel")
-
-# 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.
-#
-# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
-# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
-# is preferred for the same purpose.
-#
-# In order to load a library into your app from Java/Kotlin, you must call
-# System.loadLibrary() and pass the name of the library defined here;
-# for GameActivity/NativeActivity derived applications, the same library name must be
-# used in the AndroidManifest.xml file.
-add_library(${CMAKE_PROJECT_NAME} SHARED
- # List C/C++ source files with relative paths to this CMakeLists.txt.
- bitmap_parcel.cc
- extractors.cc
-)
-
-# Specifies libraries CMake should link to your target library. You
-# can link libraries from various origins, such as libraries defined in this
-# build script, prebuilt third-party libraries, or Android system libraries.
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log)
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE android)
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE jnigraphics)
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc b/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
deleted file mode 100644
index 036437f..0000000
--- a/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * https://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 "bitmap_parcel.h"
-
-#include <android/bitmap.h>
-#include <jni.h>
-#include <stdint.h>
-
-#include "extractors.h"
-#include "logging.h"
-
-#define LOG_TAG "bitmap_parcel"
-
-using pdflib::Extractor;
-using pdflib::BufferReader;
-using pdflib::FdReader;
-
-namespace {
-
- static const int kBytesPerPixel = 4;
-
- bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source);
-
-} // namespace
-
-extern "C" JNIEXPORT jboolean JNICALL
-Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
- (JNIEnv *env, jclass, jobject jbitmap, int fd) {
- FdReader source(fd);
- return FeedBitmap(env, jbitmap, &source);
-}
-
-namespace {
-
- bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source) {
- void *bitmap_pixels;
- int ret;
- if ((ret = AndroidBitmap_lockPixels(env, jbitmap, &bitmap_pixels)) < 0) {
- LOGE("AndroidBitmap_lockPixels() failed! error=%d", ret);
- return false;
- }
-
-
- AndroidBitmapInfo info;
- AndroidBitmap_getInfo(env, jbitmap, &info);
-
- int num_bytes = info.width * info.height * kBytesPerPixel;
- uint8_t *bitmap_bytes = reinterpret_cast<uint8_t *>(bitmap_pixels);
- source->extract(bitmap_bytes, num_bytes);
-
- AndroidBitmap_unlockPixels(env, jbitmap);
- LOGV("Copied %d bytes into bitmap", num_bytes);
- return true;
- }
-
-} // namespace
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.h b/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
deleted file mode 100644
index 8f9832e..0000000
--- a/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * https://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>
-
-#ifndef ANDROIDX_PDF_BITMAP_PARCEL_H_
-#define ANDROIDX_PDF_BITMAP_PARCEL_H_
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-JNIEXPORT jboolean JNICALL
-Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
- (JNIEnv *env, jclass, jobject jbitmap, jint jfd);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif // ANDROIDX_PDF_BITMAP_PARCEL_H_
diff --git a/pdf/pdf-viewer/src/main/native/extractors.cc b/pdf/pdf-viewer/src/main/native/extractors.cc
deleted file mode 100644
index c7ead25..0000000
--- a/pdf/pdf-viewer/src/main/native/extractors.cc
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * https://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 "extractors.h"
-
-#include <stdint.h>
-#include <unistd.h>
-
-#include <cstring>
-
-#include "logging.h"
-
-#define LOG_TAG "extractor"
-
-namespace pdflib {
-
- Extractor::~Extractor() {}
-
- BufferWriter::~BufferWriter() {}
-
- bool BufferWriter::extract(uint8_t *source, int num_bytes) {
- memcpy(buffer_, source, num_bytes);
- return true;
- }
-
- BufferReader::~BufferReader() {}
-
- bool BufferReader::extract(uint8_t *destination, int num_bytes) {
- memcpy(destination, buffer_, num_bytes);
- return true;
- }
-
- FdWriter::~FdWriter() {}
-
- bool FdWriter::extract(uint8_t *source, int num_bytes) {
- LOGV("FdWriter Extracting %d bytes on %d", num_bytes, fd_);
- bool ret = true;
- while (num_bytes > 0) {
- int len = write(fd_, source, num_bytes);
- if (len == -1 || len == 0) {
- ret = false;
- LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
- break;
- }
- num_bytes -= len;
- source += len;
- }
- close(fd_);
- return ret;
- }
-
- FdReader::~FdReader() {}
-
- bool FdReader::extract(uint8_t *destination, int num_bytes) {
- LOGV("FdReader Extracting %d bytes from %d", num_bytes, fd_);
- bool ret = true;
- while (num_bytes > 0) {
- int len = read(fd_, destination, num_bytes);
- if (len == -1 || len == 0) {
- ret = false;
- LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
- break;
- }
- num_bytes -= len;
- destination += len;
- }
- close(fd_);
- return ret;
- }
-
-} // namespace pdflib
diff --git a/pdf/pdf-viewer/src/main/native/extractors.h b/pdf/pdf-viewer/src/main/native/extractors.h
deleted file mode 100644
index 493ac51..0000000
--- a/pdf/pdf-viewer/src/main/native/extractors.h
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * https://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.
- */
-
-#ifndef ANDROIDX_PDF_EXTRACTORS_H
-#define ANDROIDX_PDF_EXTRACTORS_H
-
-#include <stdint.h>
-
-namespace pdflib {
-
-// Interface for extracting bytes from or into an underlying something.
- class Extractor {
- public:
- // Transfers {num_bytes} bytes between the underlying something and {buffer}.
- virtual ~Extractor();
-
- virtual bool extract(uint8_t *buffer, int num_bytes) = 0;
- };
-
-
-// An Extractor that copies bytes on the given buffer.
- class BufferWriter : public pdflib::Extractor {
- public:
- explicit BufferWriter(uint8_t *buffer) : buffer_(buffer) {}
-
- ~BufferWriter() override;
-
- bool extract(uint8_t *source, int num_bytes) override;
-
- private:
- uint8_t *buffer_;
- };
-
-// An Extractor that copies bytes from the given buffer.
- class BufferReader : public pdflib::Extractor {
- public:
- explicit BufferReader(uint8_t *buffer) : buffer_(buffer) {}
-
- ~BufferReader() override;
-
- bool extract(uint8_t *source, int num_bytes) override;
-
- private:
- uint8_t *buffer_;
- };
-
-// An extractor that writes bytes on the given fd. It closes the fd thereafter.
- class FdWriter : public pdflib::Extractor {
- public:
- explicit FdWriter(int fd) : fd_(fd) {}
-
- ~FdWriter() override;
-
- bool extract(uint8_t *source, int num_bytes) override;
-
- private:
- int fd_;
- };
-
-// An extractor that read bytes from the given fd. It closes the fd thereafter.
- class FdReader : public pdflib::Extractor {
- public:
- explicit FdReader(int fd) : fd_(fd) {}
-
- ~FdReader() override;
-
- bool extract(uint8_t *destination, int num_bytes) override;
-
- private:
- int fd_;
- };
-
-} // namespace pdfClient
-
-#endif // ANDROIDX_PDF_EXTRACTORS_H
diff --git a/pdf/pdf-viewer/src/main/native/logging.h b/pdf/pdf-viewer/src/main/native/logging.h
deleted file mode 100644
index a04bed4..0000000
--- a/pdf/pdf-viewer/src/main/native/logging.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * https://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.
- */
-
-#ifndef ANDROIDX_PDF_LOGGING_H
-#define ANDROIDX_PDF_LOGGING_H
-
-#include <android/log.h>
-
-#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
-#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
-#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
-
-#endif //ANDROIDX_PDF_LOGGING_H
diff --git a/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml b/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml
deleted file mode 100644
index c543141..0000000
--- a/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
- Copyright 2023 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.
- -->
-
-<resources>
-
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">@color/material_dynamic_neutral_variant10</color>
- <color name="search_textbox">@color/material_dynamic_neutral_variant20</color>
- <color name="search_texthint">@android:color/system_neutral2_200</color>
- <color name="search_textColor">@android:color/system_neutral1_100</color>
- <color name="search_count">@android:color/system_neutral2_200</color>
- <color name="search_prev_button">@android:color/system_neutral2_200</color>
- <color name="search_next_button">@android:color/system_neutral2_200</color>
- <color name="search_close_button">@android:color/system_neutral2_200</color>
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values-v31/colors.xml b/pdf/pdf-viewer/src/main/res/values-v31/colors.xml
deleted file mode 100644
index 22db258..0000000
--- a/pdf/pdf-viewer/src/main/res/values-v31/colors.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
- Copyright 2023 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.
- -->
-
-<resources>
-
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">@color/material_dynamic_neutral_variant95</color>
- <color name="search_textbox">@color/material_dynamic_neutral_variant99</color>
- <color name="search_texthint">@android:color/system_neutral2_700</color>
- <color name="search_textColor">@android:color/system_neutral1_900</color>
- <color name="search_count">@android:color/system_neutral2_700</color>
- <color name="search_prev_button">@android:color/system_neutral2_700</color>
- <color name="search_next_button">@android:color/system_neutral2_700</color>
- <color name="search_close_button">@android:color/system_neutral2_700</color>
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java
deleted file mode 100644
index 01442a8..0000000
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2024 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.pdf.mocks;
-
-import android.graphics.Bitmap;
-import android.os.ParcelFileDescriptor;
-
-import androidx.pdf.util.BitmapParcel;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-
-public class MockBitmapParcel extends BitmapParcel {
-
- private final int mBufferSize;
-
- public MockBitmapParcel(Bitmap bitmap, int bufferSize) {
- super(bitmap);
- this.mBufferSize = bufferSize;
- }
-
- @Override
- protected void receiveBitmap(Bitmap bitmap, ParcelFileDescriptor sourceFd) {
- try (FileInputStream fis = new FileInputStream(sourceFd.getFileDescriptor())) {
- FileChannel channel = fis.getChannel();
-
- ByteBuffer buffer = ByteBuffer.allocateDirect(mBufferSize);
- while (channel.read(buffer) != -1) {
- buffer.rewind();
- bitmap.copyPixelsFromBuffer(buffer);
- }
- buffer.rewind();
- channel.close();
- sourceFd.close();
- } catch (IOException ignored) {
- }
- }
-}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
index 4b29401..63f84b5 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
@@ -66,7 +66,7 @@
}
@Test
- public void launchAnnotationIntent_nonNullUri_returnsTrue() {
+ public void resolveAnnotationIntent_nonNullUri_returnsTrue() {
String fileName = "file:://dummyfile.pdf";
Uri uri = Uri.fromFile(new File(fileName));
@@ -85,23 +85,23 @@
mockResolveInfo);
when(mockContext.getPackageManager()).thenReturn(mockPackageManager);
- boolean result = AnnotationUtils.launchAnnotationIntent(mockContext, uri);
+ boolean result = AnnotationUtils.resolveAnnotationIntent(mockContext, uri);
assertTrue(result);
}
@Test
- public void launchAnnotationIntent_nullContext_returnsNullPointerException() {
+ public void resolveAnnotationIntent_nullContext_returnsNullPointerException() {
assertThrows(NullPointerException.class,
- () -> AnnotationUtils.launchAnnotationIntent(
+ () -> AnnotationUtils.resolveAnnotationIntent(
ApplicationProvider.getApplicationContext(), null));
}
@Test
- public void launchAnnotationIntent_nullUri_returnsNullPointerException() {
+ public void resolveAnnotationIntent_nullUri_returnsNullPointerException() {
String fileName = "file:://dummyfile.pdf";
Uri uri = Uri.fromFile(new File(fileName));
assertThrows(NullPointerException.class,
- () -> AnnotationUtils.launchAnnotationIntent(
+ () -> AnnotationUtils.resolveAnnotationIntent(
null, uri));
}
}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
deleted file mode 100644
index 91db95d..0000000
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2024 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.pdf.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.ParcelFileDescriptor;
-
-import androidx.pdf.mocks.MockBitmapParcel;
-import androidx.pdf.test.R;
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-
-import java.io.ByteArrayInputStream;
-import java.io.OutputStream;
-import java.nio.Buffer;
-import java.nio.ByteBuffer;
-
-//@SmallTest
-//@RunWith(RobolectricTestRunner.class)
-public class BitmapParcelTest {
-
- private static final String TAG = "BitmapParcelTest";
-
- private Bitmap mSourceBitmap;
-
- @Before
- public void setUp() {
- mSourceBitmap =
- BitmapFactory.decodeResource(
- ApplicationProvider.getApplicationContext().getResources(),
- R.raw.launcher_pdfviewer);
- }
-
- // TODO: Fails in the first execution and then passes. Uncomment when fixed
-// @Test
-// public void testBitmapTransferWithOutputFileDescriptor() {
-// int bufferSize = mSourceBitmap.getWidth() * mSourceBitmap.getHeight() * 256;
-// testBitmapTransfer(bufferSize);
-// }
-
- private void testBitmapTransfer(int bufferSize) {
- Bitmap bitmap = Bitmap.createBitmap(mSourceBitmap.getWidth(), mSourceBitmap.getHeight(),
- mSourceBitmap.getConfig());
- BitmapParcel bitmapParcel = new MockBitmapParcel(bitmap, bufferSize);
- ParcelFileDescriptor fd = bitmapParcel.openOutputFd();
- if (fd == null) {
- fail("fd is null");
- }
- sendBytes(fd);
- bitmapParcel.close();
- assertThat(validateBitmap(bitmap)).isTrue();
- }
-
- private boolean validateBitmap(Bitmap bitmap) {
- boolean same = bitmap.sameAs(mSourceBitmap);
- return same;
- }
-
- private void sendBytes(OutputStream os) {
- int numBytes = mSourceBitmap.getByteCount();
- byte[] bytes = new byte[numBytes];
- Buffer buffer = ByteBuffer.wrap(bytes);
- mSourceBitmap.copyPixelsToBuffer(buffer);
- ByteArrayInputStream is = new ByteArrayInputStream(bytes);
- Utils.copyAndClose(is, os);
- }
-
-
- private void sendBytes(ParcelFileDescriptor fd) {
- OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
- int numBytes = mSourceBitmap.getByteCount();
- byte[] bytes = new byte[numBytes];
- Buffer buffer = ByteBuffer.wrap(bytes);
- mSourceBitmap.copyPixelsToBuffer(buffer);
- ByteArrayInputStream is = new ByteArrayInputStream(bytes);
- Utils.copyAndClose(is, os);
- }
-}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
index 6f76315..e385bae 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
@@ -18,6 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
import androidx.test.filters.SmallTest;
import org.junit.Test;
@@ -52,4 +56,146 @@
assertThat(ZoomUtils.calculateZoomToFit(20, 2, 1, 0)).isEqualTo(1f);
assertThat(ZoomUtils.calculateZoomToFit(0, 0, 1, 2)).isEqualTo(0f);
}
+
+ @Test
+ public void toContentCoordinate_nonZeroZoomScroll_returnsNonZeroCoordinate() {
+ // Arrange
+ float dummyZoomViewCoordinate = 10f;
+ float dummyZoom = 2f;
+ int dummyScroll = 5;
+ float expectedContentCoordinate = 7.5f;
+
+ // Act
+ float contentCoordinate = ZoomUtils.toContentCoordinate(dummyZoomViewCoordinate,
+ dummyZoom, dummyScroll);
+
+ // Assert
+ assertThat(contentCoordinate).isEqualTo(expectedContentCoordinate);
+ }
+
+ @Test
+ public void toContentCoordinate_zeroZoom_throwsIllegalArgumentException() {
+ float dummyZoomViewCoordinate = 10f;
+ float zoom = 0;
+ int dummyScroll = 10;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> ZoomUtils.toContentCoordinate(dummyZoomViewCoordinate, zoom,
+ dummyScroll));
+ }
+
+ @Test
+ public void toZoomViewCoordinate_nonZeroZoomScroll_returnsNonZeroCoordinate() {
+ float dummyContentCoordinate = 10f;
+ float dummyZoom = 2f;
+ int dummyScroll = 5;
+ float expectedZoomViewCoordinate = 15f;
+
+ float zoomViewCoordinate = ZoomUtils.toZoomViewCoordinate(dummyContentCoordinate,
+ dummyZoom, dummyScroll);
+
+ assertThat(zoomViewCoordinate).isEqualTo(expectedZoomViewCoordinate);
+ }
+
+ @Test
+ public void toZoomViewCoordinate_zeroZoom_throwsIllegalArgumentException() {
+ float dummyContentCoordinate = 10f;
+ float zoom = 0;
+ int dummyScroll = 10;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> ZoomUtils.toZoomViewCoordinate(dummyContentCoordinate, zoom,
+ dummyScroll));
+ }
+
+ @Test
+ public void scrollDeltaNeededForZoomChange_noZoomChange_returnsZeroDelta() {
+ float oldZoom = 1f;
+ float newZoom = 1f;
+ float zoomPivot = 5f;
+ int scroll = 1;
+
+ int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+ scroll);
+
+ assertEquals(0, delta);
+ }
+
+ @Test
+ public void scrollDeltaNeededForZoomChange_zoomedIn_returnsPositiveDelta() {
+ float oldZoom = 1f;
+ float newZoom = 3f;
+ float zoomPivot = 5f;
+ int scroll = 1;
+
+ int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+ scroll);
+
+ assertTrue(delta >= 0);
+ }
+
+ @Test
+ public void scrollDeltaNeededForZoomChange_zoomedOut_returnsNegativeDelta() {
+ float oldZoom = 2f;
+ float newZoom = 1f;
+ float zoomPivot = 5f;
+ int scroll = 1;
+
+ int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+ scroll);
+
+ assertTrue(delta <= 0);
+ }
+
+ @Test
+ public void constrainCoordinate_contentLargerThanViewport_returnsZero() {
+ float zoom = 1f;
+ int scroll = 0;
+ int rawContentDimension = 5;
+ int viewportDimension = 5;
+
+ int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+ rawContentDimension, viewportDimension);
+
+ assertEquals(0, adjustedCoordinate);
+ }
+
+ @Test
+ public void constrainCoordinate_scaledContentWithinViewport_returnsNegativeAdjustedCoord() {
+ float zoom = 1f;
+ int scroll = 0;
+ int rawContentDimension = 6;
+ int viewportDimension = 8;
+
+ int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+ rawContentDimension, viewportDimension);
+
+ assertTrue(adjustedCoordinate <= 0);
+ }
+
+ @Test
+ public void constrainCoordinate_contentWithLeftDeadMargins_returnsPositiveAdjustedCoord() {
+ float zoom = 1f;
+ int scroll = -2;
+ int rawContentDimension = 6;
+ int viewportDimension = 5;
+
+ int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+ rawContentDimension, viewportDimension);
+
+ assertTrue(adjustedCoordinate >= 0);
+ }
+
+ @Test
+ public void constrainCoordinate_contentWithRightDeadMargin_returnsNegativeAdjustedCoord() {
+ float zoom = 1f;
+ int scroll = 2;
+ int rawContentDimension = 6;
+ int viewportDimension = 5;
+
+ int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+ rawContentDimension, viewportDimension);
+
+ assertTrue(adjustedCoordinate <= 0);
+ }
}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java
new file mode 100644
index 0000000..2e1dd3b
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.view.View;
+
+import androidx.pdf.widget.FastScrollView;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class FastScrollPositionValueObserverTest {
+ private final FastScrollView mMockFastScrollView = mock(FastScrollView.class);
+ private final PageIndicator mMockPageIndicator = mock(PageIndicator.class);
+ private final View mMockView = mock(View.class);
+
+ @Test
+ public void onChange() {
+ when(mMockPageIndicator.getView()).thenReturn(mMockView);
+ FastScrollPositionValueObserver fastScrollPositionValueObserver =
+ new FastScrollPositionValueObserver(mMockFastScrollView, mMockPageIndicator);
+ fastScrollPositionValueObserver.onChange(null, 100);
+ verify(mMockPageIndicator).show();
+ verify(mMockPageIndicator, times(2)).getView();
+ verify(mMockFastScrollView).setVisible();
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
index 0a45c71..7b070fc 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
@@ -29,8 +29,9 @@
public MockPageViewAccessbilityDisabledFactory(@NonNull Context context,
@NonNull PdfLoader pdfLoader,
@NonNull PaginatedView paginatedView,
- @NonNull ZoomView zoomView) {
- super(context, pdfLoader, paginatedView, zoomView);
+ @NonNull ZoomView zoomView,
+ @NonNull SingleTapHandler singleTapHandler) {
+ super(context, pdfLoader, paginatedView, zoomView, singleTapHandler);
}
@NonNull
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
index 1dd7e58..cb51642 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
@@ -31,8 +31,9 @@
@NonNull Context context,
@NonNull PdfLoader pdfLoader,
@NonNull PaginatedView paginatedView,
- @NonNull ZoomView zoomView) {
- super(context, pdfLoader, paginatedView, zoomView);
+ @NonNull ZoomView zoomView,
+ @NonNull SingleTapHandler singleTapHandler) {
+ super(context, pdfLoader, paginatedView, zoomView, singleTapHandler);
}
@NonNull
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
index 0832ad5..47cf7c0e 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
@@ -18,11 +18,24 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Rect;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.models.SelectionBoundary;
import androidx.pdf.util.BitmapRecycler;
import androidx.pdf.util.MockDrawable;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.widget.MosaicView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
@@ -33,6 +46,8 @@
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
+import java.util.List;
+
/** Unit tests for {@link PageMosaicView}. */
@SmallTest
@RunWith(RobolectricTestRunner.class)
@@ -43,6 +58,15 @@
@Mock
private BitmapRecycler mMockBitmapRecycler;
+ @Mock
+ private PdfLoader mMockPdfLoader;
+
+ @Mock
+ private PdfSelectionModel mPdfSelectionModel;
+
+ @Mock
+ private SearchModel mSearchModel;
+
@Before
public void setup() {
@@ -53,7 +77,8 @@
Dimensions dimensions = new Dimensions(800, 800);
PageMosaicView pageMosaicView = new PageMosaicView(
ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
- mMockBitmapSource, mMockBitmapRecycler);
+ mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+ mSearchModel);
assertThat(pageMosaicView.getPageText() == null).isTrue();
}
@@ -63,7 +88,8 @@
Dimensions dimensions = new Dimensions(800, 800);
PageMosaicView pageMosaicView = new PageMosaicView(
ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
- mMockBitmapSource, mMockBitmapRecycler);
+ mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+ mSearchModel);
pageMosaicView.setOverlay(new MockDrawable());
assertThat(pageMosaicView.hasOverlay()).isTrue();
@@ -74,7 +100,8 @@
Dimensions dimensions = new Dimensions(800, 800);
PageMosaicView pageMosaicView = new PageMosaicView(
ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
- mMockBitmapSource, mMockBitmapRecycler);
+ mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+ mSearchModel);
pageMosaicView.setOverlay(new MockDrawable());
assertThat(pageMosaicView.hasOverlay()).isTrue();
@@ -88,7 +115,8 @@
Dimensions dimensions = new Dimensions(800, 800);
PageMosaicView pageMosaicView = new PageMosaicView(
ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
- mMockBitmapSource, mMockBitmapRecycler) {
+ mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+ mSearchModel) {
@Override
@NonNull
protected String buildContentDescription(@Nullable String pageText, int pageNum) {
@@ -108,7 +136,8 @@
Dimensions dimensions = new Dimensions(800, 800);
PageMosaicView pageMosaicView = new PageMosaicView(
ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
- mMockBitmapSource, mMockBitmapRecycler) {
+ mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+ mSearchModel) {
@Override
@NonNull
protected String buildContentDescription(@Nullable String pageText, int pageNum) {
@@ -120,4 +149,133 @@
assertThat(pageMosaicView.getPageText() == null).isTrue();
assertThat(pageMosaicView.getContentDescription()).isEqualTo(dummyContentDescription);
}
+
+ @Test
+ public void refreshPageContentAndOverlays_needsPageText_callsLoadPageTextRemovesOverlay() {
+ PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+ PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+ SearchModel dummySearchModel = mock(SearchModel.class);
+ when(dummyPdfSelectionModel.getPage()).thenReturn(1);
+ when(dummySearchModel.query()).thenReturn(new ObservableValue<String>() {
+ @Nullable
+ @Override
+ public String get() {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public Object addObserver(ValueObserver<String> observer) {
+ return null;
+ }
+
+ @Override
+ public void removeObserver(@NonNull Object observerKey) {
+
+ }
+ });
+
+ Dimensions dimensions = new Dimensions(800, 800);
+ PageMosaicView pageMosaicView = new PageMosaicView(
+ ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+ mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+ dummySearchModel) {
+ @Override
+ public boolean needsPageText() {
+ return true;
+ }
+ };
+
+ pageMosaicView.refreshPageContentAndOverlays();
+
+ verify(dummyPdfLoader).loadPageText(any(Integer.class));
+ verify(dummyPdfLoader).loadPageUrlLinks(any(Integer.class));
+ verify(dummyPdfLoader).loadPageGotoLinks(any(Integer.class));
+ assertFalse(pageMosaicView.hasOverlay());
+ }
+
+ @Test
+ public void refreshPageContentAndOverlays_sameSelectionPage_setsHighlightOverlay() {
+ List<Rect> boundingBoxes = List.of(new Rect(10, 10, 10, 10));
+ PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+ PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+ SearchModel dummySearchModel = mock(SearchModel.class);
+ when(dummyPdfSelectionModel.getPage()).thenReturn(0);
+ when(dummyPdfSelectionModel.selection()).thenReturn(new ObservableValue<PageSelection>() {
+
+ @NonNull
+ @Override
+ public Object addObserver(ValueObserver<PageSelection> observer) {
+ return null;
+ }
+
+ @Override
+ public void removeObserver(@NonNull Object observerKey) {
+
+ }
+
+ @Nullable
+ @Override
+ public PageSelection get() {
+ return new PageSelection(0, mock(SelectionBoundary.class),
+ mock(SelectionBoundary.class), boundingBoxes, "");
+ }
+ });
+
+ Dimensions dimensions = new Dimensions(800, 800);
+ PageMosaicView pageMosaicView = new PageMosaicView(
+ ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+ mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+ dummySearchModel) {
+ @Override
+ public boolean needsPageText() {
+ return false;
+ }
+ };
+
+ pageMosaicView.refreshPageContentAndOverlays();
+
+ assertTrue(pageMosaicView.hasOverlay());
+ }
+
+ @Test
+ public void refreshPageContentAndOverlays_nonNullSearchQuery_callsSearchPageText() {
+ PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+ PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+ SearchModel dummySearchModel = mock(SearchModel.class);
+ when(dummyPdfSelectionModel.getPage()).thenReturn(1);
+ when(dummySearchModel.query()).thenReturn(new ObservableValue<String>() {
+ @Nullable
+ @Override
+ public String get() {
+ return "placeholder";
+ }
+
+ @NonNull
+ @Override
+ public Object addObserver(ValueObserver<String> observer) {
+ return null;
+ }
+
+ @Override
+ public void removeObserver(@NonNull Object observerKey) {
+
+ }
+ });
+
+ Dimensions dimensions = new Dimensions(800, 800);
+ PageMosaicView pageMosaicView = new PageMosaicView(
+ ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+ mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+ dummySearchModel) {
+ @Override
+ public boolean needsPageText() {
+ return false;
+ }
+ };
+
+ pageMosaicView.refreshPageContentAndOverlays();
+
+ verify(dummyPdfLoader).searchPageText(any(Integer.class), any(String.class));
+ }
}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java
new file mode 100644
index 0000000..da59d113
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import androidx.pdf.data.Range;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class PageRangeHandlerTest {
+ @Test
+ public void getVisiblePage_nullVisiblePageRange_returnsZero() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ int expectedResult = 0;
+
+ int result = adapter.getVisiblePage();
+
+ assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void getVisiblePage_nonNullRange_returnsMidOfRange() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ Range dummyVisiblePageRange = new Range(1, 4);
+ int expectedResult = 2;
+
+ adapter.setVisiblePages(dummyVisiblePageRange);
+ int result = adapter.getVisiblePage();
+
+ assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void adjustMaxPageToUpperVisibleRange_nullVisiblePageRange_noChangeInMaxPage() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ int expectedResult = -1;
+
+ adapter.adjustMaxPageToUpperVisibleRange();
+ int result = adapter.getMaxPage();
+
+ assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void adjustMaxPageToUpperVisibleRange_lowerUpperVisibleRange_noChangeInMaxPage() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ Range dummyVisiblePageRange = new Range(1, 4);
+ int dummyMaxPage = 10;
+
+ adapter.setVisiblePages(dummyVisiblePageRange);
+ adapter.setMaxPage(dummyMaxPage);
+ adapter.adjustMaxPageToUpperVisibleRange();
+ int result = adapter.getMaxPage();
+
+ assertThat(result).isEqualTo(dummyMaxPage);
+ }
+
+ @Test
+ public void adjustMaxPageToUpperVisibleRange_higherUpperVisibleRange_updatedMaxPage() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ Range dummyVisiblePageRange = new Range(1, 10);
+ int dummyMaxPage = 4;
+ int expectedResult = 10;
+
+ adapter.setVisiblePages(dummyVisiblePageRange);
+ adapter.setMaxPage(dummyMaxPage);
+ adapter.adjustMaxPageToUpperVisibleRange();
+ int result = adapter.getMaxPage();
+
+ assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void computeVisibleRange_zeroZoom_throwsIllegalArgumentException() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ int dummyScrollY = 10;
+ float dummyZoom = 0;
+ int dummyViewHeight = 100;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> adapter.computeVisibleRange(dummyScrollY, dummyZoom, dummyViewHeight, true));
+ }
+
+ @Test
+ public void computeVisibleRange_overlappingRange_returnsOverlappedRange() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ when(mockPaginationModel.getPagesInWindow(any(Range.class), any(Boolean.class))).thenAnswer(
+ new Answer<Range>() {
+ private final Range mDefaultRange = new Range(3, 5);
+
+ @Override
+ public Range answer(InvocationOnMock invocation) throws Throwable {
+ Range intervalPx = invocation.getArgument(0);
+ boolean includePartial = invocation.getArgument(1);
+
+ // Condition has been added so that we can capture a change to the value
+ if (includePartial) {
+ return new Range(
+ Math.min(intervalPx.getFirst(), mDefaultRange.getFirst()),
+ Math.max(intervalPx.getLast(), mDefaultRange.getLast())
+ );
+ }
+ return mDefaultRange;
+
+ }
+ });
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ int dummyScrollY = 2;
+ float dummyZoom = 2;
+ int dummyViewHeight = 10;
+ Range expectedResult = new Range(1, 6);
+
+ Range result = adapter.computeVisibleRange(dummyScrollY, dummyZoom, dummyViewHeight, true);
+
+ assertEquals(result, expectedResult);
+ }
+
+ @Test
+ public void refreshVisiblePageRange_overlappingRange_updatesVisibleRangeWithOverlappedRange() {
+ PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+ when(mockPaginationModel.getPagesInWindow(any(Range.class), any(Boolean.class))).thenAnswer(
+ new Answer<Range>() {
+ private final Range mDefaultRange = new Range(3, 5);
+
+ @Override
+ public Range answer(InvocationOnMock invocation) throws Throwable {
+ Range intervalPx = invocation.getArgument(0);
+ boolean includePartial = invocation.getArgument(1);
+
+ // Condition has been added so that we can capture a change to the value
+ if (includePartial) {
+ return new Range(
+ Math.min(intervalPx.getFirst(), mDefaultRange.getFirst()),
+ Math.max(intervalPx.getLast(), mDefaultRange.getLast())
+ );
+ }
+ return mDefaultRange;
+
+ }
+ });
+ PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+ int dummyScrollY = 2;
+ float dummyZoom = 2;
+ int dummyViewHeight = 10;
+ Range expectedResult = new Range(1, 6);
+
+ adapter.refreshVisiblePageRange(dummyScrollY, dummyZoom, dummyViewHeight);
+ Range result = adapter.getVisiblePages();
+
+ assertEquals(result, expectedResult);
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
new file mode 100644
index 0000000..1927d8b
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.PageSelection;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class PageSelectionValueObserverTest {
+ private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+ private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+ private final PageViewFactory mMockPageViewFactory = mock(PageViewFactory.class);
+ private final PageSelection mMockOldPageSelection = mock(PageSelection.class);
+ private final PageSelection mMockNewPageSelection = mock(PageSelection.class);
+ private final PageViewFactory.PageView mMockOldPageView = mock(PageViewFactory.PageView.class);
+ private final PageViewFactory.PageView mMockNewPageView = mock(PageViewFactory.PageView.class);
+ private final PageMosaicView mMockOldPageMosaicView = mock(PageMosaicView.class);
+ private final PageMosaicView mMockNewPageMosaicView = mock(PageMosaicView.class);
+ private final PageRangeHandler mMockPageRangeHandler = mock(PageRangeHandler.class);
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void onChange_setOverlay() {
+ when(mMockOldPageSelection.getPage()).thenReturn(1);
+ when(mMockNewPageSelection.getPage()).thenReturn(2);
+ when(mMockPaginationModel.getSize()).thenReturn(3);
+ when(mMockPaginationModel.getPageSize(2)).thenReturn(new Dimensions(100, 100));
+ when(mMockPaginatedView.getViewAt(1)).thenReturn(mMockOldPageView);
+ when(mMockPaginatedView.getViewAt(2)).thenReturn(mMockNewPageView);
+ when(mMockOldPageView.getPageView()).thenReturn(mMockOldPageMosaicView);
+ when(mMockPageViewFactory.getOrCreatePageView(2, 2, new Dimensions(100, 100))).thenReturn(
+ mMockNewPageMosaicView);
+ when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mMockPageRangeHandler);
+ when(mMockPageRangeHandler.getVisiblePages()).thenReturn(new Range(1, 2));
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ displayMetrics.density = 1f;
+ mContext.getResources().getDisplayMetrics().setTo(displayMetrics);
+ PdfViewer.setScreenForTest(mContext);
+
+ PageSelectionValueObserver pageSelectionValueObserver =
+ new PageSelectionValueObserver(mMockPaginatedView, mMockPaginationModel,
+ mMockPageViewFactory, mContext);
+ pageSelectionValueObserver.onChange(mMockOldPageSelection, mMockNewPageSelection);
+
+ verify(mMockOldPageMosaicView).setOverlay(null);
+ verify(mMockNewPageMosaicView).setOverlay(any());
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
index d14175d..c3869d4 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
@@ -50,8 +50,7 @@
private final ZoomView mMockZoomView = mock(ZoomView.class);
- private final PdfViewer.PageTouchHandler mMockPageTouchHandler = mock(
- PdfViewer.PageTouchHandler.class);
+ private final SingleTapHandler mMockSingleTapHandler = mock(SingleTapHandler.class);
@Before
public void setup() {
@@ -69,12 +68,11 @@
PageViewFactory.PageView.class);
PageViewFactory mockPageViewFactory = new MockPageViewAccessbilityDisabledFactory(
ApplicationProvider.getApplicationContext(), mMockPdfLoader, mMockPaginatedView,
- mMockZoomView
+ mMockZoomView, mMockSingleTapHandler
);
// Act
- mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions,
- mMockPageTouchHandler);
+ mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions);
// Assert
verify(mMockPaginatedView).addView(pageViewArgumentCaptor.capture());
@@ -101,12 +99,11 @@
PageViewFactory.PageView.class);
PageViewFactory mockPageViewFactory = new MockPageViewAccessbilityEnabledFactory(
ApplicationProvider.getApplicationContext(), mMockPdfLoader, mMockPaginatedView,
- mMockZoomView
+ mMockZoomView, mMockSingleTapHandler
);
// Act
- mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions,
- mMockPageTouchHandler);
+ mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions);
// Assert
verify(mMockPaginatedView).addView(pageViewArgumentCaptor.capture());
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
index a7f335d..1ebf1c7 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
@@ -23,6 +23,7 @@
import androidx.pdf.models.Dimensions;
import androidx.pdf.util.BitmapRecycler;
import androidx.pdf.viewer.PageViewFactory.PageView;
+import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.widget.MosaicView.BitmapSource;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
@@ -52,6 +53,12 @@
BitmapSource mMockBitmapSource;
@Mock
BitmapRecycler mMockBitmapRecycler;
+ @Mock
+ PdfLoader mMockPdfLoader;
+ @Mock
+ PdfSelectionModel mPdfSelectionModel;
+ @Mock
+ SearchModel mSearchModel;
PageView mTestPageView0;
PageView mTestPageView1;
@@ -71,9 +78,9 @@
mPaginatedView.setModel(mPaginationModel);
mTestPageView0 = new PageMosaicView(mContext, 0, mDimensions, mMockBitmapSource,
- mMockBitmapRecycler);
+ mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
mTestPageView1 = new PageMosaicView(mContext, 1, mDimensions, mMockBitmapSource,
- mMockBitmapRecycler);
+ mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
}
@After
@@ -98,7 +105,7 @@
mPaginationModel.addPage(2, mDimensions);
PageView testPageView3 = new PageMosaicView(mContext, 2, mDimensions, mMockBitmapSource,
- mMockBitmapRecycler);
+ mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
mPaginatedView.addView(testPageView3);
mPaginatedView.removeViewAt(1);
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java
new file mode 100644
index 0000000..d726676
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class SearchQueryObserverTest {
+
+ private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+
+ @Test
+ public void onChange() {
+ SearchQueryObserver searchQueryObserver = new SearchQueryObserver(mMockPaginatedView);
+ searchQueryObserver.onChange("oldQuery", "newQuery");
+ verify(mMockPaginatedView).clearAllOverlays();
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
new file mode 100644
index 0000000..7ac5b06
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.MatchRects;
+import androidx.pdf.widget.ZoomView;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class SelectedMatchValueObserverTest {
+ private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+ private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+ private final PageViewFactory mMockPageViewFactory = mock(PageViewFactory.class);
+ private final ZoomView mMockZoomView = mock(ZoomView.class);
+ private final LayoutHandler mMockLayoutHandler = mock(LayoutHandler.class);
+ private final SelectedMatch mMockOldSelection = mock(SelectedMatch.class);
+ private final SelectedMatch mMockNewSelection = mock(SelectedMatch.class);
+ private final PageViewFactory.PageView mMockOldPageView = mock(PageViewFactory.PageView.class);
+ private final PageViewFactory.PageView mMockNewPageView = mock(PageViewFactory.PageView.class);
+ private final PageMosaicView mMockOldPageMosaicView = mock(PageMosaicView.class);
+ private final PageMosaicView mMockNewPageMosaicView = mock(PageMosaicView.class);
+ private final MatchRects mMockMatchRects = mock(MatchRects.class);
+ private final PdfHighlightOverlay mMockPdfHighlightOverlay = mock(PdfHighlightOverlay.class);
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void onChange_setOverlay() {
+ when(mMockOldSelection.getPage()).thenReturn(1);
+ when(mMockNewSelection.getPage()).thenReturn(2);
+ when(mMockPaginationModel.getSize()).thenReturn(3);
+ when(mMockPaginatedView.getViewAt(1)).thenReturn(mMockOldPageView);
+ when(mMockPaginatedView.getViewAt(2)).thenReturn(mMockNewPageView);
+ when(mMockPaginationModel.getPageSize(2)).thenReturn(new Dimensions(100, 100));
+ when(mMockOldPageView.getPageView()).thenReturn(mMockOldPageMosaicView);
+ when(mMockPageViewFactory.getOrCreatePageView(2, 2, new Dimensions(100, 100))).thenReturn(
+ mMockNewPageMosaicView);
+ when(mMockOldSelection.getPageMatches()).thenReturn(
+ new MatchRects(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()));
+ when(mMockNewSelection.getPageMatches()).thenReturn(mMockMatchRects);
+ when(mMockNewSelection.getSelected()).thenReturn(1);
+ when(mMockNewSelection.getPage()).thenReturn(2);
+ when(mMockMatchRects.getFirstRect(1)).thenReturn(new Rect(0, 0, 0, 0));
+ when(mMockPaginationModel.getLookAtX(2, 0)).thenReturn(0);
+ when(mMockPaginationModel.getLookAtY(2, 0)).thenReturn(0);
+ when(mMockNewSelection.getOverlay()).thenReturn(mMockPdfHighlightOverlay);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ displayMetrics.density = 1f;
+ mContext.getResources().getDisplayMetrics().setTo(displayMetrics);
+ PdfViewer.setScreenForTest(mContext);
+
+ SelectedMatchValueObserver selectedMatchValueObserver = new SelectedMatchValueObserver(
+ mMockPaginatedView, mMockPaginationModel, mMockPageViewFactory, mMockZoomView,
+ mMockLayoutHandler, mContext);
+ selectedMatchValueObserver.onChange(mMockOldSelection, mMockNewSelection);
+
+ verify(mMockZoomView).centerAt(0, 0);
+ verify(mMockNewPageMosaicView).setOverlay(mMockPdfHighlightOverlay);
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
new file mode 100644
index 0000000..f9c46a9
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.Observables;
+import androidx.pdf.widget.FastScrollView;
+import androidx.pdf.widget.ZoomView;
+import androidx.test.filters.SmallTest;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class ZoomScrollValueObserverTest {
+ private static final ObservableValue<Viewer.ViewState> VIEW_STATE_EXPOSED_VALUE =
+ Observables.newExposedValueWithInitialValue(Viewer.ViewState.NO_VIEW);
+ private static final Rect RECT = new Rect(0, 0, 100, 100);
+ private static final ZoomView.ZoomScroll OLD_POSITION = new ZoomView.ZoomScroll(1.0f, 0, 0,
+ false);
+ private static final Range PAGE_RANGE = new Range(0, 100);
+
+ private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+ private final ZoomView mMockZoomView = mock(ZoomView.class);
+ private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+ private final LayoutHandler mMockLayoutHandler = mock(LayoutHandler.class);
+ private final FloatingActionButton mMockAnnotationButton = mock(FloatingActionButton.class);
+ private final FindInFileView mMockFindInFileView = mock(FindInFileView.class);
+ private final PageIndicator mMockPageIndicator = mock(PageIndicator.class);
+ private final FastScrollView mMockFastScrollView = mock(FastScrollView.class);
+ private final PageRangeHandler mPageRangeHandler = mock(PageRangeHandler.class);
+
+ private boolean mIsAnnotationIntentResolvable;
+ private ZoomView.ZoomScroll mNewPosition;
+
+
+ @Before
+ public void setUp() {
+ mIsAnnotationIntentResolvable = false;
+ mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, false);
+
+ when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mPageRangeHandler);
+ when(mMockPaginatedView.getPaginationModel()).thenReturn(mMockPaginationModel);
+ when(mMockZoomView.getHeight()).thenReturn(100);
+ when(mPageRangeHandler.computeVisibleRange(0, 1.0f, 100, false)).thenReturn(PAGE_RANGE);
+ when(mMockPageIndicator.setRangeAndZoom(PAGE_RANGE, 1.0f, true)).thenReturn(false);
+ when(mMockZoomView.getStableZoom()).thenReturn(1.0f);
+ when(mMockZoomView.getVisibleAreaInContentCoords()).thenReturn(RECT);
+ when(mMockPaginatedView.createPageViewsForVisiblePageRange()).thenReturn(false);
+ when(mPageRangeHandler.getVisiblePages()).thenReturn(PAGE_RANGE);
+ }
+
+ @Test
+ public void onChange_loadPageAssets_stablePosition() {
+ mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, true);
+
+ ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+ mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+ mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+ mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+ zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+ verify(mMockZoomView).setStableZoom(1.0f);
+ verify(mMockPaginationModel).setViewArea(RECT);
+ verify(mMockPaginatedView).refreshPageRangeInVisibleArea(mNewPosition, 100);
+ verify(mMockPaginatedView).handleGonePages(false);
+ verify(mMockPaginatedView).loadInvisibleNearPageRange(1.0f);
+ verify(mMockPaginatedView).refreshVisiblePages(false, Viewer.ViewState.NO_VIEW, 1.0f);
+ verify(mMockPaginatedView).handleGonePages(true);
+ verify(mMockLayoutHandler).maybeLayoutPages(100);
+ }
+
+ @Test
+ public void onChange_loadPageAssets_stableZoom() {
+ mNewPosition = new ZoomView.ZoomScroll(2.0f, 0, 0, false);
+ when(mMockZoomView.getStableZoom()).thenReturn(2.0f);
+
+ ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+ mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+ mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+ mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+ zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+ verify(mMockPaginatedView).refreshVisibleTiles(false, Viewer.ViewState.NO_VIEW);
+ }
+
+ @Test
+ public void onChange_showFastScrollView() {
+ when(mMockPageIndicator.setRangeAndZoom(PAGE_RANGE, 1.0f, false)).thenReturn(true);
+
+ ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+ mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+ mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+ mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+ zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+ verify(mMockFastScrollView).setVisible();
+ }
+
+ @Test
+ public void onChange_showAnnotationButton() {
+ mIsAnnotationIntentResolvable = true;
+ when(mMockAnnotationButton.getVisibility()).thenReturn(View.GONE);
+ when(mMockFindInFileView.getVisibility()).thenReturn(View.GONE);
+
+ ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+ mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+ mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+ mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+ zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+ verify(mMockAnnotationButton).setVisibility(View.VISIBLE);
+ }
+
+ @Test
+ public void onChange_hideAnnotationButton() {
+ mIsAnnotationIntentResolvable = true;
+ mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 1, false);
+
+ ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+ mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+ mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+ mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+ zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+ verify(mMockAnnotationButton).setVisibility(View.GONE);
+ }
+
+}
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index c4d6592..0e954eb 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -55,6 +55,7 @@
}
defaultConfig {
+ compileSdk = 35
testInstrumentationRunner "androidx.recyclerview.test.TestRunner"
}
namespace "androidx.recyclerview"
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
new file mode 100644
index 0000000..1972555
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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.recyclerview.widget
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.TextView
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+// TODO: change to VANILLA_ICE_CREAM when it is ready
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(AndroidJUnit4::class)
+class RecyclerViewScrollFrameRateTest {
+ @get:Rule val rule = ActivityTestRule(TestContentViewActivity::class.java)
+
+ @Test
+ fun smoothScrollFrameRateBoost() {
+ // TODO: Remove when VANILLA_ICE_CREAM is ready and the SdkSuppress is modified
+ if (!BuildCompat.isAtLeastV()) {
+ return
+ }
+ val rv = RecyclerView(rule.activity)
+ rule.runOnUiThread {
+ rv.layoutManager =
+ LinearLayoutManager(rule.activity, LinearLayoutManager.VERTICAL, false)
+ rv.adapter =
+ object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): RecyclerView.ViewHolder {
+ val view = TextView(parent.context)
+ view.textSize = 40f
+ view.setTextColor(Color.WHITE)
+ return object : RecyclerView.ViewHolder(view) {}
+ }
+
+ override fun getItemCount(): Int = 10000
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val view = holder.itemView as TextView
+ view.text = "Text $position"
+ val color = if (position % 2 == 0) Color.BLACK else 0xFF000080.toInt()
+ view.setBackgroundColor(color)
+ }
+ }
+ rule.activity.contentView.addView(rv)
+ }
+ runOnDraw(rv, { rv.smoothScrollBy(0, 1000) }) {
+ // First Frame
+ assertThat(rv.frameContentVelocity).isGreaterThan(0f)
+ }
+
+ // Second frame
+ runOnDraw(rv) { assertThat(rv.frameContentVelocity).isGreaterThan(0f) }
+
+ // Third frame
+ runOnDraw(rv) { assertThat(rv.frameContentVelocity).isGreaterThan(0f) }
+ }
+
+ private fun runOnDraw(view: View, setup: () -> Unit = {}, onDraw: () -> Unit) {
+ val latch = CountDownLatch(1)
+ val onDrawListener =
+ ViewTreeObserver.OnDrawListener {
+ latch.countDown()
+ onDraw()
+ }
+ rule.runOnUiThread {
+ view.viewTreeObserver.addOnDrawListener(onDrawListener)
+ setup()
+ }
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
+ rule.runOnUiThread { view.viewTreeObserver.removeOnDrawListener(onDrawListener) }
+ }
+}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 80be2f0..9d4ef74 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -64,12 +64,15 @@
import android.widget.OverScroller;
import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
import androidx.core.os.TraceCompat;
import androidx.core.util.Preconditions;
import androidx.core.view.AccessibilityDelegateCompat;
@@ -6005,6 +6008,10 @@
mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
}
}
+ if (BuildCompat.isAtLeastV()) {
+ Api35Impl.setFrameContentVelocity(RecyclerView.this,
+ Math.abs(scroller.getCurrVelocity()));
+ }
}
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
@@ -14627,4 +14634,16 @@
}
return mScrollingChildHelper;
}
+
+ @RequiresApi(35)
+ private static final class Api35Impl {
+ @DoNotInline
+ public static void setFrameContentVelocity(View view, float velocity) {
+ try {
+ view.setFrameContentVelocity(velocity);
+ } catch (LinkageError e) {
+ // The setFrameContentVelocity method is unavailable on this device.
+ }
+ }
+ }
}
diff --git a/samples/AndroidXDemos/build.gradle b/samples/AndroidXDemos/build.gradle
index 519d459..048ac9c 100644
--- a/samples/AndroidXDemos/build.gradle
+++ b/samples/AndroidXDemos/build.gradle
@@ -41,6 +41,7 @@
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
}
}
+ compileSdkVersion 35
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
diff --git a/samples/MediaRoutingDemo/build.gradle b/samples/MediaRoutingDemo/build.gradle
index 6da205a..617f702 100644
--- a/samples/MediaRoutingDemo/build.gradle
+++ b/samples/MediaRoutingDemo/build.gradle
@@ -28,6 +28,7 @@
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
}
}
+ compileSdk 35
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
index aed9f13..cb1f13b 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
@@ -44,7 +44,7 @@
public class AddEditRouteActivity extends AppCompatActivity {
private static final String EXTRA_ROUTE_ID_KEY = "routeId";
- private SampleDynamicGroupMediaRouteProviderService mService;
+ @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
private ServiceConnection mConnection;
private RoutesManager mRoutesManager;
private RouteItem mRouteItem;
@@ -163,7 +163,9 @@
saveButton.setOnClickListener(
view -> {
mRoutesManager.addRoute(mRouteItem);
- mService.reloadRoutes();
+ if (mService != null) {
+ mService.reloadRoutes();
+ }
finish();
});
}
@@ -203,7 +205,7 @@
}
@Override
- public void onServiceDisconnected(ComponentName arg0) {
+ public void onServiceDisconnected(ComponentName unusedComponentName) {
mService = null;
}
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
index 65e89dd..97dd936 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
@@ -648,6 +648,9 @@
@Override
public void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) {
Log.d(TAG, "onRouteChanged: route=" + route);
+ if (route.isSelected()) {
+ updateRouteDescription();
+ }
}
@Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
index 2db33b8..1285855 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
@@ -43,6 +44,7 @@
import com.example.androidx.mediarouting.RoutesManager;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutingActivity;
import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
+import com.example.androidx.mediarouting.services.SampleMediaRouteProviderService;
import com.example.androidx.mediarouting.ui.RoutesAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -52,20 +54,20 @@
* SampleDynamicGroupMediaRouteProviderService}.
*/
public final class SettingsActivity extends AppCompatActivity {
+ private final ProviderServiceConnection mConnection = new ProviderServiceConnection();
+ private PackageManager mPackageManager;
private MediaRouter mMediaRouter;
private RoutesManager mRoutesManager;
private RoutesAdapter mRoutesAdapter;
- private SampleDynamicGroupMediaRouteProviderService mService;
- private ServiceConnection mConnection;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
+ mPackageManager = getPackageManager();
mMediaRouter = MediaRouter.getInstance(this);
mRoutesManager = RoutesManager.getInstance(getApplicationContext());
- mConnection = new ProviderServiceConnection();
setUpViews();
@@ -86,7 +88,11 @@
android.R.string.ok,
(dialogInterface, i) -> {
mRoutesManager.deleteRouteWithId(routeId);
- mService.reloadRoutes();
+ SampleDynamicGroupMediaRouteProviderService providerService =
+ mConnection.mService;
+ if (providerService != null) {
+ providerService.reloadRoutes();
+ }
mRoutesAdapter.updateRoutes(
mRoutesManager.getRouteItems());
})
@@ -111,10 +117,7 @@
@Override
protected void onStart() {
super.onStart();
- // Bind to SampleDynamicGroupMediaRouteProviderService
- Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
- intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
- bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ bindToDynamicProviderService();
}
@Override
@@ -125,13 +128,20 @@
@Override
protected void onStop() {
+ try {
+ unbindService(mConnection);
+ } catch (RuntimeException e) {
+ // This happens when the provider is disabled, but there's no way of preventing this
+ // completely so we just ignore the exception.
+ }
super.onStop();
- unbindService(mConnection);
}
private void setUpViews() {
setUpDynamicGroupsEnabledSwitch();
setUpTransferToLocalSwitch();
+ setUpSimpleProviderEnabledSwitch();
+ setUpDynamicProviderEnabledSwitch();
setUpDialogTypeDropDownList();
setUpNewRouteButton();
setupSystemRoutesButton();
@@ -141,9 +151,13 @@
Switch dynamicRoutingEnabled = findViewById(R.id.dynamic_routing_switch);
dynamicRoutingEnabled.setChecked(mRoutesManager.isDynamicRoutingEnabled());
dynamicRoutingEnabled.setOnCheckedChangeListener(
- (compoundButton, b) -> {
- mRoutesManager.setDynamicRoutingEnabled(b);
- mService.reloadDynamicRoutesEnabled();
+ (compoundButton, enabled) -> {
+ mRoutesManager.setDynamicRoutingEnabled(enabled);
+ SampleDynamicGroupMediaRouteProviderService providerService =
+ mConnection.mService;
+ if (providerService != null) {
+ providerService.reloadDynamicRoutesEnabled();
+ }
});
}
@@ -151,14 +165,56 @@
Switch showThisPhoneSwitch = findViewById(R.id.show_this_phone_switch);
showThisPhoneSwitch.setChecked(mMediaRouter.getRouterParams().isTransferToLocalEnabled());
showThisPhoneSwitch.setOnCheckedChangeListener(
- (compoundButton, b) -> {
+ (compoundButton, enabled) -> {
MediaRouterParams.Builder builder =
new MediaRouterParams.Builder(mMediaRouter.getRouterParams());
- builder.setTransferToLocalEnabled(b);
+ builder.setTransferToLocalEnabled(enabled);
mMediaRouter.setRouterParams(builder.build());
});
}
+ private void setUpSimpleProviderEnabledSwitch() {
+ Switch simpleProviderEnabledSwitch = findViewById(R.id.enable_simple_provider_switch);
+ ComponentName simpleProviderComponentName =
+ new ComponentName(/* context= */ this, SampleMediaRouteProviderService.class);
+ simpleProviderEnabledSwitch.setChecked(
+ mPackageManager.getComponentEnabledSetting(simpleProviderComponentName)
+ != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+ simpleProviderEnabledSwitch.setOnCheckedChangeListener(
+ (compoundButton, enabled) -> {
+ mPackageManager
+ .setComponentEnabledSetting(
+ simpleProviderComponentName,
+ enabled
+ ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ /* flags= */ PackageManager.DONT_KILL_APP);
+ });
+ }
+
+ private void setUpDynamicProviderEnabledSwitch() {
+ Switch dynamicProviderEnabledSwitch = findViewById(R.id.enable_dynamic_provider_switch);
+ ComponentName dynamicProviderComponentName =
+ new ComponentName(
+ /* context= */ this, SampleDynamicGroupMediaRouteProviderService.class);
+ dynamicProviderEnabledSwitch.setChecked(
+ mPackageManager.getComponentEnabledSetting(dynamicProviderComponentName)
+ != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+ dynamicProviderEnabledSwitch.setOnCheckedChangeListener(
+ (compoundButton, enabled) -> {
+ mPackageManager
+ .setComponentEnabledSetting(
+ dynamicProviderComponentName,
+ enabled
+ ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ /* flags= */ PackageManager.DONT_KILL_APP);
+ if (enabled) {
+ bindToDynamicProviderService();
+ }
+ });
+ }
+
private void setUpDialogTypeDropDownList() {
Spinner spinner = findViewById(R.id.dialog_spinner);
spinner.setOnItemSelectedListener(
@@ -204,13 +260,22 @@
showSystemRoutesButton.setOnClickListener(v -> SystemRoutingActivity.launch(this));
}
- private class ProviderServiceConnection implements ServiceConnection {
+ private void bindToDynamicProviderService() {
+ Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
+ intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private static class ProviderServiceConnection implements ServiceConnection {
+
+ @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
SampleDynamicGroupMediaRouteProviderService.LocalBinder binder =
(SampleDynamicGroupMediaRouteProviderService.LocalBinder) service;
mService = binder.getService();
+ mService.reloadRoutes();
}
@Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
index c92ced8..0f331cd 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
@@ -19,71 +19,66 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
import java.util.Objects;
-import javax.annotation.Nullable;
-
-/**
- * An abstract model that holds information about routes from different sources.
- *
- * Can represent media routers' routes, bluetooth routes, or audio routes.
- */
+/** Holds information about a system route. */
public final class SystemRouteItem implements SystemRoutesAdapterItem {
- @NonNull
- private final String mId;
+ /**
+ * Describes the support of a route for selection.
+ *
+ * <p>We understand by selection the action that makes a specific route the active route. Note
+ * that this terminology may not match the terminology used by the underlying {@link
+ * SystemRoutesSource}.
+ */
+ public enum SelectionSupportState {
+ /** The underlying route source doesn't support selection. */
+ UNSUPPORTED,
+ /**
+ * The corresponding route is already selected, but can be reselected.
+ *
+ * <p>Selecting an already selected route (reselection) can change the metadata of the route
+ * source. For example, reselecting a MediaRouter2 route can alter the transfer reason.
+ */
+ RESELECTABLE,
+ /** The route is available for selection. */
+ SELECTABLE
+ }
- @NonNull
- private final String mName;
+ /** The {@link SystemRoutesSource#getSourceId()} of the source that created this item. */
+ @NonNull public final String mSourceId;
- @Nullable
- private final String mAddress;
+ /** An id that uniquely identifies this route item within the source. */
+ @NonNull public final String mId;
- @Nullable
- private final String mDescription;
+ @NonNull public final String mName;
+
+ @Nullable public final String mAddress;
+
+ @Nullable public final String mDescription;
+
+ @Nullable public final String mSuitabilityStatus;
+
+ @Nullable public final Boolean mTransferInitiatedBySelf;
+
+ @Nullable public final String mTransferReason;
+
+ @NonNull public final SelectionSupportState mSelectionSupportState;
private SystemRouteItem(@NonNull Builder builder) {
- Objects.requireNonNull(builder.mId);
- Objects.requireNonNull(builder.mName);
-
- mId = builder.mId;
- mName = builder.mName;
-
+ mSourceId = Objects.requireNonNull(builder.mSourceId);
+ mId = Objects.requireNonNull(builder.mId);
+ mName = Objects.requireNonNull(builder.mName);
mAddress = builder.mAddress;
mDescription = builder.mDescription;
- }
-
- /**
- * Returns a unique identifier of a route.
- */
- @NonNull
- public String getId() {
- return mId;
- }
-
- /**
- * Returns a human-readable name of the route.
- */
- @NonNull
- public String getName() {
- return mName;
- }
-
- /**
- * Returns address if the route is a Bluetooth route and {@code null} otherwise.
- */
- @Nullable
- public String getAddress() {
- return mAddress;
- }
-
- /**
- * Returns a route description or {@code null} if empty.
- */
- @Nullable
- public String getDescription() {
- return mDescription;
+ mSuitabilityStatus = builder.mSuitabilityStatus;
+ mTransferInitiatedBySelf = builder.mTransferInitiatedBySelf;
+ mTransferReason = builder.mTransferReason;
+ mSelectionSupportState = builder.mSelectionSupportState;
}
@Override
@@ -91,14 +86,29 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SystemRouteItem that = (SystemRouteItem) o;
- return mId.equals(that.mId) && mName.equals(that.mName)
- && Objects.equals(mAddress, that.mAddress) && Objects.equals(
- mDescription, that.mDescription);
+ return mSourceId.equals(that.mSourceId)
+ && mId.equals(that.mId)
+ && mName.equals(that.mName)
+ && Objects.equals(mAddress, that.mAddress)
+ && Objects.equals(mDescription, that.mDescription)
+ && Objects.equals(mSuitabilityStatus, that.mSuitabilityStatus)
+ && Objects.equals(mTransferInitiatedBySelf, that.mTransferInitiatedBySelf)
+ && Objects.equals(mTransferReason, that.mTransferReason)
+ && mSelectionSupportState.equals(that.mSelectionSupportState);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mName, mAddress, mDescription);
+ return Objects.hash(
+ mSourceId,
+ mId,
+ mName,
+ mAddress,
+ mDescription,
+ mSuitabilityStatus,
+ mTransferInitiatedBySelf,
+ mTransferReason,
+ mSelectionSupportState);
}
/**
@@ -106,20 +116,26 @@
*/
public static final class Builder {
- @NonNull
- private final String mId;
+ @NonNull private String mSourceId;
+ @NonNull private final String mId;
+ @NonNull private String mName;
+ @Nullable private String mAddress;
+ @Nullable private String mDescription;
+ @Nullable private String mSuitabilityStatus;
+ @Nullable private Boolean mTransferInitiatedBySelf;
+ @Nullable private String mTransferReason;
+ @NonNull public SelectionSupportState mSelectionSupportState;
- @NonNull
- private String mName;
-
- @Nullable
- private String mAddress;
-
- @Nullable
- private String mDescription;
-
- public Builder(@NonNull String id) {
+ /**
+ * Creates a builder with the mandatory properties.
+ *
+ * @param sourceId See {@link SystemRouteItem#mSourceId}.
+ * @param id See {@link SystemRouteItem#mId}.
+ */
+ public Builder(@NonNull String sourceId, @NonNull String id) {
+ mSourceId = sourceId;
mId = id;
+ mSelectionSupportState = SelectionSupportState.UNSUPPORTED;
}
/**
@@ -154,6 +170,43 @@
}
/**
+ * Sets a human-readable string describing the transfer suitability of the route, or null if
+ * not applicable.
+ */
+ @NonNull
+ public Builder setSuitabilityStatus(@Nullable String suitabilityStatus) {
+ mSuitabilityStatus = suitabilityStatus;
+ return this;
+ }
+
+ /**
+ * Sets whether the corresponding route's selection is the result of an action of this app,
+ * or null if not applicable.
+ */
+ @NonNull
+ public Builder setTransferInitiatedBySelf(@Nullable Boolean transferInitiatedBySelf) {
+ mTransferInitiatedBySelf = transferInitiatedBySelf;
+ return this;
+ }
+
+ /**
+ * Sets a human-readable string describing the transfer reason, or null if not applicable.
+ */
+ @NonNull
+ public Builder setTransferReason(@Nullable String transferReason) {
+ mTransferReason = transferReason;
+ return this;
+ }
+
+ /** Sets the {@link SelectionSupportState} for the corresponding route. */
+ @NonNull
+ public Builder setSelectionSupportState(
+ @NonNull SelectionSupportState selectionSupportState) {
+ mSelectionSupportState = Objects.requireNonNull(selectionSupportState);
+ return this;
+ }
+
+ /**
* Builds {@link SystemRouteItem}.
*/
@NonNull
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
index aba49a1..764c050 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
@@ -16,14 +16,21 @@
package com.example.androidx.mediarouting.activities.systemrouting;
+import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.SELECTABLE;
+import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.UNSUPPORTED;
+
+import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.util.Consumer;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
@@ -32,17 +39,19 @@
import java.util.List;
-/**
- * @link RecyclerView.Adapter} for showing system route sources and the routes discovered by each
- * source.
- */
-class SystemRoutesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+/** {@link RecyclerView.Adapter} for showing system route sources and their corresponding routes. */
+/* package */ class SystemRoutesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final AsyncListDiffer<SystemRoutesAdapterItem> mListDiffer =
new AsyncListDiffer<>(this, new ItemCallback());
+ private final Consumer<SystemRouteItem> mRouteItemClickedListener;
+
+ /* package */ SystemRoutesAdapter(Consumer<SystemRouteItem> routeItemClickListener) {
+ mRouteItemClickedListener = routeItemClickListener;
+ }
public void setItems(@NonNull List<SystemRoutesAdapterItem> newItems) {
mListDiffer.submitList(newItems);
@@ -108,12 +117,16 @@
}
}
- static class ItemViewHolder extends RecyclerView.ViewHolder {
+ private class ItemViewHolder extends RecyclerView.ViewHolder {
private final AppCompatTextView mRouteNameTextView;
private final AppCompatTextView mRouteIdTextView;
private final AppCompatTextView mRouteAddressTextView;
private final AppCompatTextView mRouteDescriptionTextView;
+ private final AppCompatTextView mSuitabilityStatusTextView;
+ private final AppCompatTextView mTransferInitiatedBySelfTextView;
+ private final AppCompatTextView mTransferReasonTextView;
+ private final AppCompatButton mSelectionButton;
ItemViewHolder(@NonNull View itemView) {
super(itemView);
@@ -122,20 +135,41 @@
mRouteIdTextView = itemView.findViewById(R.id.route_id);
mRouteAddressTextView = itemView.findViewById(R.id.route_address);
mRouteDescriptionTextView = itemView.findViewById(R.id.route_description);
+ mSuitabilityStatusTextView = itemView.findViewById(R.id.route_suitability_status);
+ mTransferInitiatedBySelfTextView =
+ itemView.findViewById(R.id.route_transfer_initiated_by_self);
+ mTransferReasonTextView = itemView.findViewById(R.id.route_transfer_reason);
+ mSelectionButton = itemView.findViewById(R.id.route_selection_button);
}
void bind(SystemRouteItem systemRouteItem) {
- mRouteNameTextView.setText(systemRouteItem.getName());
- mRouteIdTextView.setText(systemRouteItem.getId());
+ mRouteNameTextView.setText(systemRouteItem.mName);
+ mRouteIdTextView.setText(systemRouteItem.mId);
+ setTextOrHide(mRouteAddressTextView, systemRouteItem.mAddress);
+ setTextOrHide(mRouteDescriptionTextView, systemRouteItem.mDescription);
+ setTextOrHide(mSuitabilityStatusTextView, systemRouteItem.mSuitabilityStatus);
+ String initiatedBySelfText =
+ systemRouteItem.mTransferInitiatedBySelf != null
+ ? "self-initiated: " + systemRouteItem.mTransferInitiatedBySelf
+ : null;
+ setTextOrHide(mTransferInitiatedBySelfTextView, initiatedBySelfText);
+ String transferReasonText =
+ systemRouteItem.mTransferReason != null
+ ? "transfer reason: " + systemRouteItem.mTransferReason
+ : null;
+ setTextOrHide(mTransferReasonTextView, transferReasonText);
- showViewIfNotNull(mRouteAddressTextView, systemRouteItem.getAddress());
- if (systemRouteItem.getAddress() != null) {
- mRouteAddressTextView.setText(systemRouteItem.getAddress());
- }
-
- showViewIfNotNull(mRouteDescriptionTextView, systemRouteItem.getDescription());
- if (systemRouteItem.getDescription() != null) {
- mRouteDescriptionTextView.setText(systemRouteItem.getDescription());
+ if (systemRouteItem.mSelectionSupportState == UNSUPPORTED) {
+ mSelectionButton.setVisibility(View.GONE);
+ } else {
+ mSelectionButton.setVisibility(View.VISIBLE);
+ String text =
+ systemRouteItem.mSelectionSupportState == SELECTABLE
+ ? "Select"
+ : "Reselect";
+ mSelectionButton.setText(text);
+ mSelectionButton.setOnClickListener(
+ view -> mRouteItemClickedListener.accept(systemRouteItem));
}
}
}
@@ -145,8 +179,7 @@
public boolean areItemsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
@NonNull SystemRoutesAdapterItem newItem) {
if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
- return ((SystemRouteItem) oldItem).getId().equals(
- ((SystemRouteItem) newItem).getId());
+ return ((SystemRouteItem) oldItem).mId.equals(((SystemRouteItem) newItem).mId);
} else if (oldItem instanceof SystemRoutesSourceItem
&& newItem instanceof SystemRoutesSourceItem) {
return oldItem.equals(newItem);
@@ -155,25 +188,21 @@
}
}
+ @SuppressLint("DiffUtilEquals")
@Override
- public boolean areContentsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
+ public boolean areContentsTheSame(
+ @NonNull SystemRoutesAdapterItem oldItem,
@NonNull SystemRoutesAdapterItem newItem) {
- if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
- return oldItem.equals(newItem);
- } else if (oldItem instanceof SystemRoutesSourceItem
- && newItem instanceof SystemRoutesSourceItem) {
- return oldItem.equals(newItem);
- } else {
- return false;
- }
+ return oldItem.equals(newItem);
}
}
- private static <T, V extends View> void showViewIfNotNull(@NonNull V view, @Nullable T obj) {
- if (obj == null) {
+ private static void setTextOrHide(@NonNull TextView view, @Nullable String text) {
+ if (text == null) {
view.setVisibility(View.GONE);
} else {
view.setVisibility(View.VISIBLE);
+ view.setText(text);
}
}
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
index 73d57fe..6f18b57 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
@@ -44,7 +44,9 @@
import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* Shows available system routes gathered from different sources.
@@ -54,15 +56,11 @@
private static final int REQUEST_CODE_BLUETOOTH_CONNECT = 4199;
@NonNull
- private final SystemRoutesAdapter mSystemRoutesAdapter = new SystemRoutesAdapter();
- @NonNull
- private final List<SystemRoutesSource> mSystemRoutesSources = new ArrayList<>();
- @NonNull
- private final SystemRoutesSourceCallback mSystemRoutesSourceCallback =
- new SystemRoutesSourceCallback();
+ private final SystemRoutesAdapter mSystemRoutesAdapter =
+ new SystemRoutesAdapter(this::onRouteItemClicked);
- @NonNull
- private SwipeRefreshLayout mSwipeRefreshLayout;
+ @NonNull private final Map<String, SystemRoutesSource> mSystemRoutesSources = new HashMap<>();
+ @NonNull private SwipeRefreshLayout mSwipeRefreshLayout;
/**
* Creates and launches an intent to start current activity.
@@ -95,7 +93,7 @@
@Override
protected void onDestroy() {
- for (SystemRoutesSource source: mSystemRoutesSources) {
+ for (SystemRoutesSource source : mSystemRoutesSources.values()) {
source.stop();
}
@@ -119,7 +117,7 @@
private void refreshSystemRoutesList() {
List<SystemRoutesAdapterItem> systemRoutesSourceItems = new ArrayList<>();
- for (SystemRoutesSource source : mSystemRoutesSources) {
+ for (SystemRoutesSource source : mSystemRoutesSources.values()) {
systemRoutesSourceItems.add(source.getSourceItem());
systemRoutesSourceItems.addAll(source.fetchSourceRouteItems());
}
@@ -127,6 +125,20 @@
mSwipeRefreshLayout.setRefreshing(false);
}
+ private void onRouteItemClicked(SystemRouteItem item) {
+ SystemRoutesSource systemRoutesSource = mSystemRoutesSources.get(item.mSourceId);
+ if (systemRoutesSource == null) {
+ throw new IllegalStateException("Couldn't find source with id: " + item.mSourceId);
+ }
+ if (!systemRoutesSource.select(item)) {
+ Toast.makeText(
+ /* context= */ this,
+ "Something went wrong with route selection",
+ Toast.LENGTH_LONG)
+ .show();
+ }
+ }
+
private boolean hasBluetoothPermission() {
return ContextCompat.checkSelfPermission(/* context= */ this, BLUETOOTH_CONNECT)
== PERMISSION_GRANTED
@@ -150,40 +162,28 @@
}
private void initializeSystemRoutesSources() {
- mSystemRoutesSources.clear();
-
- mSystemRoutesSources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
+ ArrayList<SystemRoutesSource> sources = new ArrayList<>();
+ sources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- mSystemRoutesSources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
+ sources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
}
- mSystemRoutesSources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
+ sources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
if (hasBluetoothPermission()) {
- mSystemRoutesSources.add(
- BluetoothManagerSystemRoutesSource.create(/* context= */ this));
+ sources.add(BluetoothManagerSystemRoutesSource.create(/* context= */ this));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- mSystemRoutesSources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
+ sources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
}
- for (SystemRoutesSource source: mSystemRoutesSources) {
- source.setOnRoutesChangedListener(mSystemRoutesSourceCallback);
+ mSystemRoutesSources.clear();
+ for (SystemRoutesSource source : sources) {
+ source.setOnRoutesChangedListener(this::refreshSystemRoutesList);
+ mSystemRoutesSources.put(source.getSourceId(), source);
source.start();
}
}
-
- private class SystemRoutesSourceCallback implements SystemRoutesSource.OnRoutesChangedListener {
- @Override
- public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
- refreshSystemRoutesList();
- }
-
- @Override
- public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
- refreshSystemRoutesList();
- }
- }
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
index 7e553ed..d7763d0 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
@@ -36,19 +36,43 @@
private final MediaRouter mMediaRouter;
@NonNull
- private final MediaRouter.Callback mMediaRouterCallback = new MediaRouter.Callback() {
- @Override
- public void onRouteAdded(@NonNull MediaRouter router,
- @NonNull MediaRouter.RouteInfo route) {
- mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
- }
+ private final MediaRouter.Callback mMediaRouterCallback =
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteAdded(
+ @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+ mOnRoutesChangedListener.run();
+ }
- @Override
- public void onRouteRemoved(@NonNull MediaRouter router,
- @NonNull MediaRouter.RouteInfo route) {
- mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
- }
- };
+ @Override
+ public void onRouteRemoved(
+ @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteSelected(
+ @NonNull MediaRouter router,
+ @NonNull MediaRouter.RouteInfo selectedRoute,
+ int reason,
+ @NonNull MediaRouter.RouteInfo requestedRoute) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteUnselected(
+ @NonNull MediaRouter router,
+ @NonNull MediaRouter.RouteInfo route,
+ int reason) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteChanged(
+ @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+ mOnRoutesChangedListener.run();
+ }
+ };
/** Returns a new instance. */
@NonNull
@@ -98,11 +122,27 @@
return out;
}
- @NonNull
- private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
- SystemRouteItem.Builder builder = new SystemRouteItem.Builder(routeInfo.getId())
- .setName(routeInfo.getName());
+ @Override
+ public boolean select(@NonNull SystemRouteItem item) {
+ for (MediaRouter.RouteInfo routeInfo : mMediaRouter.getRoutes()) {
+ if (routeInfo.getId().equals(item.mId)) {
+ routeInfo.select();
+ return true;
+ }
+ }
+ return false;
+ }
+ @NonNull
+ private SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
+ SystemRouteItem.Builder builder =
+ new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
+ .setName(routeInfo.getName());
+
+ builder.setSelectionSupportState(
+ routeInfo.isSelected()
+ ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+ : SystemRouteItem.SelectionSupportState.SELECTABLE);
String description = routeInfo.getDescription();
if (description != null) {
builder.setDescription(description);
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
index 096ebfd..9e7b67a 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
@@ -39,21 +39,18 @@
private final AudioManager mAudioManager;
@NonNull
- private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallback() {
- @Override
- public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
- for (AudioDeviceInfo audioDeviceInfo: addedDevices) {
- mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(audioDeviceInfo));
- }
- }
+ private final AudioDeviceCallback mAudioDeviceCallback =
+ new AudioDeviceCallback() {
+ @Override
+ public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+ mOnRoutesChangedListener.run();
+ }
- @Override
- public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
- for (AudioDeviceInfo audioDeviceInfo: removedDevices) {
- mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(audioDeviceInfo));
- }
- }
- };
+ @Override
+ public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+ mOnRoutesChangedListener.run();
+ }
+ };
/** Returns a new instance. */
@NonNull
@@ -95,11 +92,16 @@
return out;
}
+ @Override
+ public boolean select(@NonNull SystemRouteItem item) {
+ throw new UnsupportedOperationException();
+ }
+
@NonNull
- private static SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
- SystemRouteItem.Builder builder = new SystemRouteItem.Builder(
- String.valueOf(audioDeviceInfo.getId()))
- .setName(audioDeviceInfo.getProductName().toString());
+ private SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
+ SystemRouteItem.Builder builder =
+ new SystemRouteItem.Builder(getSourceId(), String.valueOf(audioDeviceInfo.getId()))
+ .setName(audioDeviceInfo.getProductName().toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setAddress(Api28Impl.getAddress(audioDeviceInfo));
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
index 96fab5b..7bc34d7 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
@@ -23,7 +23,6 @@
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -31,7 +30,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
-import androidx.core.content.IntentCompat;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
@@ -104,10 +102,15 @@
return out;
}
+ @Override
+ public boolean select(@NonNull SystemRouteItem item) {
+ throw new UnsupportedOperationException();
+ }
+
@NonNull
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
- private static SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
- return new SystemRouteItem.Builder(/* id= */ device.getAddress())
+ private SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
+ return new SystemRouteItem.Builder(getSourceId(), /* id= */ device.getAddress())
.setName(device.getName())
.setAddress(device.getAddress())
.build();
@@ -117,27 +120,14 @@
@Override
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
public void onReceive(Context context, Intent intent) {
- BluetoothDevice device = IntentCompat.getParcelableExtra(intent,
- BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
-
switch (intent.getAction()) {
case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
- handleConnectionStateChanged(intent, device);
+ mOnRoutesChangedListener.run();
break;
- }
- }
-
- @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
- private void handleConnectionStateChanged(Intent intent,
- BluetoothDevice device) {
- int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
- if (state == BluetoothProfile.STATE_CONNECTED) {
- mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(device));
- } else if (state == BluetoothProfile.STATE_DISCONNECTING
- || state == BluetoothProfile.STATE_DISCONNECTED) {
- mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(device));
+ default:
+ // Do nothing.
}
}
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
index 5785679..1c85f85 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
@@ -16,59 +16,61 @@
package com.example.androidx.mediarouting.activities.systemrouting.source;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.RouteDiscoveryPreference;
+import android.media.RoutingSessionInfo;
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.Arrays;
import java.util.List;
-import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
/** Implements {@link SystemRoutesSource} using {@link MediaRouter2}. */
@RequiresApi(Build.VERSION_CODES.R)
public final class MediaRouter2SystemRoutesSource extends SystemRoutesSource {
- @NonNull
- private Context mContext;
- @NonNull
- private MediaRouter2 mMediaRouter2;
+ @NonNull private final Context mContext;
+ @NonNull private final MediaRouter2 mMediaRouter2;
+ @Nullable private final Method mSuitabilityStatusMethod;
+ @Nullable private final Method mWasTransferInitiatedBySelfMethod;
+ @Nullable private final Method mTransferReasonMethod;
+ @NonNull private final ArrayList<SystemRouteItem> mRouteItems = new ArrayList<>();
@NonNull
- private final Map<String, MediaRoute2Info> mLastKnownRoutes = new HashMap<>();
- @NonNull
- private final MediaRouter2.RouteCallback mRouteCallback = new MediaRouter2.RouteCallback() {
- @Override
- public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
- super.onRoutesUpdated(routes);
-
- Map<String, MediaRoute2Info> routesLookup = new HashMap<>();
- for (MediaRoute2Info route: routes) {
- if (!mLastKnownRoutes.containsKey(route.getId())) {
- mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
+ private final MediaRouter2.RouteCallback mRouteCallback =
+ new MediaRouter2.RouteCallback() {
+ @Override
+ public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
+ populateRouteItems(routes);
}
- routesLookup.put(route.getId(), route);
- }
+ };
- for (MediaRoute2Info route: mLastKnownRoutes.values()) {
- if (!routesLookup.containsKey(route.getId())) {
- mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
+ @NonNull
+ private final MediaRouter2.ControllerCallback mControllerCallback =
+ new MediaRouter2.ControllerCallback() {
+ @Override
+ public void onControllerUpdated(
+ @NonNull MediaRouter2.RoutingController unusedController) {
+ populateRouteItems(mMediaRouter2.getRoutes());
}
- }
-
- mLastKnownRoutes.clear();
- mLastKnownRoutes.putAll(routesLookup);
- }
- };
+ };
/** Returns a new instance. */
@NonNull
@@ -81,22 +83,45 @@
@NonNull MediaRouter2 mediaRouter2) {
mContext = context;
mMediaRouter2 = mediaRouter2;
+
+ Method suitabilityStatusMethod = null;
+ Method wasTransferInitiatedBySelfMethod = null;
+ Method transferReasonMethod = null;
+ // TODO: b/336510942 - Remove reflection once these APIs are available in
+ // androidx-platform-dev.
+ try {
+ suitabilityStatusMethod =
+ MediaRoute2Info.class.getDeclaredMethod("getSuitabilityStatus");
+ wasTransferInitiatedBySelfMethod =
+ MediaRouter2.RoutingController.class.getDeclaredMethod(
+ "wasTransferInitiatedBySelf");
+ transferReasonMethod = RoutingSessionInfo.class.getDeclaredMethod("getTransferReason");
+ } catch (NoSuchMethodException | IllegalAccessError e) {
+ }
+ mSuitabilityStatusMethod = suitabilityStatusMethod;
+ mWasTransferInitiatedBySelfMethod = wasTransferInitiatedBySelfMethod;
+ mTransferReasonMethod = transferReasonMethod;
}
@Override
public void start() {
RouteDiscoveryPreference routeDiscoveryPreference =
new RouteDiscoveryPreference.Builder(
- /* preferredFeatures= */ Collections.emptyList(),
- /* activeScan= */ false)
+ /* preferredFeatures= */ Arrays.asList(
+ MediaRoute2Info.FEATURE_LIVE_AUDIO,
+ MediaRoute2Info.FEATURE_LIVE_VIDEO),
+ /* activeScan= */ false)
.build();
- mMediaRouter2.registerRouteCallback(mContext.getMainExecutor(),
- mRouteCallback, routeDiscoveryPreference);
+ Executor mainExecutor = mContext.getMainExecutor();
+ mMediaRouter2.registerRouteCallback(mainExecutor, mRouteCallback, routeDiscoveryPreference);
+ mMediaRouter2.registerControllerCallback(mainExecutor, mControllerCallback);
+ populateRouteItems(mMediaRouter2.getRoutes());
}
@Override
public void stop() {
+ mMediaRouter2.unregisterControllerCallback(mControllerCallback);
mMediaRouter2.unregisterRouteCallback(mRouteCallback);
}
@@ -109,28 +134,141 @@
@NonNull
@Override
public List<SystemRouteItem> fetchSourceRouteItems() {
- List<SystemRouteItem> out = new ArrayList<>();
+ return mRouteItems;
+ }
- for (MediaRoute2Info routeInfo : mMediaRouter2.getRoutes()) {
- if (!routeInfo.isSystemRoute()) {
- continue;
- }
-
- if (!mLastKnownRoutes.containsKey(routeInfo.getId())) {
- mLastKnownRoutes.put(routeInfo.getId(), routeInfo);
- }
-
- out.add(createRouteItemFor(routeInfo));
+ @Override
+ public boolean select(@NonNull SystemRouteItem item) {
+ Optional<MediaRoute2Info> route =
+ mMediaRouter2.getRoutes().stream()
+ .filter(it -> it.getId().equals(item.mId))
+ .findFirst();
+ if (!route.isPresent()) {
+ return false;
+ } else {
+ mMediaRouter2.transferTo(route.get());
+ return true;
}
+ }
- return out;
+ // BanUncheckedReflection: See b/336510942 for details on why reflection is needed.
+ // NewApi: We don't need to check the API level because the transfer reason method is only
+ // available on API 35, which is greater than API 34, where getRoutingSessionInfo was added.
+ @SuppressLint({"BanUncheckedReflection", "NewApi"})
+ private void populateRouteItems(List<MediaRoute2Info> routes) {
+ MediaRouter2.RoutingController systemController = mMediaRouter2.getSystemController();
+ Set<String> selectedRoutesIds =
+ systemController.getSelectedRoutes().stream()
+ .map(MediaRoute2Info::getId)
+ .collect(Collectors.toSet());
+ Boolean selectionInitiatedBySelf = null;
+ Integer sessionTransferReason = null;
+ try {
+ if (mSuitabilityStatusMethod != null) {
+ selectionInitiatedBySelf =
+ (Boolean) mWasTransferInitiatedBySelfMethod.invoke(systemController);
+ }
+ if (mTransferReasonMethod != null) {
+ sessionTransferReason =
+ (Integer)
+ mTransferReasonMethod.invoke(
+ Api34Impl.getRoutingSessionInfo(systemController));
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ }
+ // We need to filter out non-system routes, which might be reported as a result of other
+ // callbacks with non-system route features being registered in the router.
+ List<MediaRoute2Info> systemRoutes =
+ routes.stream().filter(MediaRoute2Info::isSystemRoute).collect(Collectors.toList());
+
+ mRouteItems.clear();
+ for (MediaRoute2Info route : systemRoutes) {
+ boolean isSelectedRoute = selectedRoutesIds.contains(route.getId());
+ Boolean wasTransferredBySelf = isSelectedRoute ? selectionInitiatedBySelf : null;
+ Integer routeTransferReason = isSelectedRoute ? sessionTransferReason : null;
+ mRouteItems.add(
+ createRouteItemFor(
+ route, isSelectedRoute, wasTransferredBySelf, routeTransferReason));
+ }
+ mOnRoutesChangedListener.run();
}
@NonNull
- private static SystemRouteItem createRouteItemFor(@NonNull MediaRoute2Info routeInfo) {
- return new SystemRouteItem.Builder(routeInfo.getId())
- .setName(String.valueOf(routeInfo.getName()))
- .setDescription(String.valueOf(routeInfo.getDescription()))
- .build();
+ private SystemRouteItem createRouteItemFor(
+ @NonNull MediaRoute2Info routeInfo,
+ boolean isSelectedRoute,
+ @Nullable Boolean wasTransferredBySelf,
+ @Nullable Integer transferReason) {
+ SystemRouteItem.Builder builder =
+ new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
+ .setName(String.valueOf(routeInfo.getName()))
+ .setSelectionSupportState(
+ isSelectedRoute
+ ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+ : SystemRouteItem.SelectionSupportState.SELECTABLE)
+ .setDescription(String.valueOf(routeInfo.getDescription()))
+ .setTransferInitiatedBySelf(wasTransferredBySelf)
+ .setTransferReason(getHumanReadableTransferReason(transferReason));
+ try {
+ if (mSuitabilityStatusMethod != null) {
+ // See b/336510942 for details on why reflection is needed.
+ @SuppressLint("BanUncheckedReflection")
+ int status = (Integer) mSuitabilityStatusMethod.invoke(routeInfo);
+ builder.setSuitabilityStatus(getHumanReadableSuitabilityStatus(status));
+ // TODO: b/319645714 - Populate wasTransferInitiatedBySelf. For that we need to
+ // change the implementation of this class to use the routing controller instead
+ // of a route callback.
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ }
+ return builder.build();
+ }
+
+ @NonNull
+ private String getHumanReadableSuitabilityStatus(@Nullable Integer status) {
+ if (status == null) {
+ // The route is not selected, or this Android version doesn't support suitability
+ // status.
+ return null;
+ }
+ switch (status) {
+ case 0:
+ return "SUITABLE_FOR_DEFAULT_TRANSFER";
+ case 1:
+ return "SUITABLE_FOR_MANUAL_TRANSFER";
+ case 2:
+ return "NOT_SUITABLE_FOR_TRANSFER";
+ default:
+ return "UNKNOWN(" + status + ")";
+ }
+ }
+
+ @NonNull
+ private String getHumanReadableTransferReason(@Nullable Integer transferReason) {
+ if (transferReason == null) {
+ // The route is not selected, or this Android version doesn't support transfer reason.
+ return null;
+ }
+ switch (transferReason) {
+ case 0:
+ return "FALLBACK";
+ case 1:
+ return "SYSTEM_REQUEST";
+ case 2:
+ return "APP";
+ default:
+ return "UNKNOWN(" + transferReason + ")";
+ }
+ }
+
+ @RequiresApi(34)
+ private static final class Api34Impl {
+ private Api34Impl() {}
+
+ @DoNotInline
+ static RoutingSessionInfo getRoutingSessionInfo(
+ MediaRouter2.RoutingController routingController) {
+ return routingController.getRoutingSessionInfo();
+ }
}
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
index 438ccfc..21d112f 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.media.MediaRouter;
+import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -34,19 +35,35 @@
private final MediaRouter mMediaRouter;
@NonNull
- private final MediaRouter.Callback mCallback = new MediaRouter.SimpleCallback() {
- @Override
- public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
- super.onRouteAdded(router, info);
- mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(info));
- }
+ private final MediaRouter.Callback mCallback =
+ new MediaRouter.SimpleCallback() {
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ mOnRoutesChangedListener.run();
+ }
- @Override
- public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
- super.onRouteRemoved(router, info);
- mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(info));
- }
- };
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteUnselected(
+ MediaRouter router, int type, MediaRouter.RouteInfo info) {
+ mOnRoutesChangedListener.run();
+ }
+
+ @Override
+ public void onRouteSelected(
+ MediaRouter router, int type, MediaRouter.RouteInfo info) {
+ mOnRoutesChangedListener.run();
+ }
+ };
/** Returns a new instance. */
@NonNull
@@ -83,23 +100,42 @@
List<SystemRouteItem> out = new ArrayList<>();
+ MediaRouter.RouteInfo selectedRoute =
+ mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
for (int i = 0; i < count; i++) {
MediaRouter.RouteInfo info = mMediaRouter.getRouteAt(i);
if (info.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL) {
// We are only interested in system routes.
- out.add(createRouteItemFor(info));
+ out.add(createRouteItemFor(info, /* isSelected= */ selectedRoute == info));
}
}
return out;
}
- @NonNull
- private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
- SystemRouteItem.Builder builder =
- new SystemRouteItem.Builder(/* id= */ routeInfo.getName().toString())
- .setName(routeInfo.getName().toString());
+ @Override
+ public boolean select(@NonNull SystemRouteItem item) {
+ int routeCount = mMediaRouter.getRouteCount();
+ for (int i = 0; i < routeCount; i++) {
+ MediaRouter.RouteInfo route = mMediaRouter.getRouteAt(i);
+ if (TextUtils.equals(route.getName().toString(), item.mId)) {
+ mMediaRouter.selectRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO, route);
+ return true;
+ }
+ }
+ return false;
+ }
+ @NonNull
+ private SystemRouteItem createRouteItemFor(
+ @NonNull MediaRouter.RouteInfo routeInfo, boolean isSelected) {
+ SystemRouteItem.Builder builder =
+ new SystemRouteItem.Builder(getSourceId(), /* id= */ routeInfo.getName().toString())
+ .setName(routeInfo.getName().toString());
+ builder.setSelectionSupportState(
+ isSelected
+ ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+ : SystemRouteItem.SelectionSupportState.SELECTABLE);
CharSequence description = routeInfo.getDescription();
if (description != null) {
builder.setDescription(String.valueOf(description));
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
index f5a32db..bad6317 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
@@ -17,7 +17,6 @@
package com.example.androidx.mediarouting.activities.systemrouting.source;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
@@ -29,23 +28,11 @@
*/
public abstract class SystemRoutesSource {
- private static final NoOpOnRoutesChangedListener NO_OP_ON_ROUTES_CHANGED_LISTENER =
- new NoOpOnRoutesChangedListener();
+ @NonNull protected Runnable mOnRoutesChangedListener = () -> {};
- @NonNull
- protected OnRoutesChangedListener mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
-
- /**
- * Sets {@link OnRoutesChangedListener} and subscribes to the source updates.
- * To unsubscribe from the routes update pass {@code null} instead of the listener.
- */
- public void setOnRoutesChangedListener(
- @Nullable OnRoutesChangedListener onRoutesChangedListener) {
- if (onRoutesChangedListener != null) {
- mOnRoutesChangedListener = onRoutesChangedListener;
- } else {
- mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
- }
+ /** Sets a {@link Runnable} to invoke whenever routes change. */
+ public void setOnRoutesChangedListener(@NonNull Runnable onRoutesChangedListener) {
+ mOnRoutesChangedListener = onRoutesChangedListener;
}
/**
@@ -63,6 +50,12 @@
// Empty on purpose.
}
+ /** Returns a string that uniquely identifies this source. */
+ @NonNull
+ public final String getSourceId() {
+ return getClass().getSimpleName();
+ }
+
/**
* Gets a source item containing source type.
*/
@@ -76,39 +69,11 @@
public abstract List<SystemRouteItem> fetchSourceRouteItems();
/**
- * An interface for listening to routes changes: whether the route has been added or removed
- * from the source.
+ * Selects the route that corresponds to the given item.
+ *
+ * @param item An item with {@link SystemRouteItem#mSelectionSupportState} {@link
+ * SystemRouteItem.SelectionSupportState#SELECTABLE}.
+ * @return Whether the selection was successful.
*/
- public interface OnRoutesChangedListener {
-
- /**
- * Called when a route has been added to the source's routes list.
- *
- * @param routeItem a newly added route.
- */
- void onRouteAdded(@NonNull SystemRouteItem routeItem);
-
- /**
- * Called when a route has been removed from the source's routes list.
- *
- * @param routeItem a recently removed route.
- */
- void onRouteRemoved(@NonNull SystemRouteItem routeItem);
- }
-
- /**
- * Default no-op implementation of {@link OnRoutesChangedListener}.
- * Used as a fallback implement when there is no listener.
- */
- private static final class NoOpOnRoutesChangedListener implements OnRoutesChangedListener {
- @Override
- public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
- // Empty on purpose.
- }
-
- @Override
- public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
- // Empty on purpose.
- }
- }
+ public abstract boolean select(@NonNull SystemRouteItem item);
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
index 47ffed2..a597796 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
@@ -304,7 +304,16 @@
for (String memberRouteId : mMemberRouteIds) {
groupDescriptorBuilder.addGroupMemberId(memberRouteId);
}
-
+ if (!mMemberRouteIds.isEmpty()) {
+ DynamicRouteDescriptor firstDynamicRouteDescriptor =
+ mDynamicRouteDescriptors.get(mMemberRouteIds.get(0));
+ if (firstDynamicRouteDescriptor != null) {
+ String name = firstDynamicRouteDescriptor.getRouteDescriptor().getName();
+ int sizeMinusOne = mMemberRouteIds.size() - 1;
+ String nameSuffix = sizeMinusOne == 0 ? "" : (" + " + sizeMinusOne);
+ groupDescriptorBuilder.setName(name + nameSuffix);
+ }
+ }
mGroupDescriptor = groupDescriptorBuilder.build();
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
index 658c256..abbfbf2 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
@@ -58,12 +58,16 @@
/** Reload all routes provided by this service. */
public void reloadRoutes() {
- mDynamicGroupMediaRouteProvider.reloadRoutes();
+ if (mDynamicGroupMediaRouteProvider != null) {
+ mDynamicGroupMediaRouteProvider.reloadRoutes();
+ }
}
/** Reload the flag for isDynamicRouteEnabled. */
public void reloadDynamicRoutesEnabled() {
- mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
+ if (mDynamicGroupMediaRouteProvider != null) {
+ mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
+ }
}
/**
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
index 2572fc6..50418cb 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
@@ -91,6 +91,56 @@
android:layout_margin="12dp"
android:padding="4dp">
+ <Switch
+ android:id="@+id/enable_simple_provider_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:gravity="center"
+ android:text="Enable simple route provider" />
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <Switch
+ android:id="@+id/enable_dynamic_provider_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:gravity="center"
+ android:text="Enable dynamic route provider" />
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
<Spinner
android:id="@+id/dialog_spinner"
android:layout_width="wrap_content"
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
index 5d54748..8b3ac4e 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
@@ -77,6 +77,49 @@
android:textSize="14sp"
tools:text="This is a description of an amazing system route." />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_suitability_status"
+ android:layout_marginTop="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_description"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="14sp"
+ tools:text="Suitability status." />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_transfer_initiated_by_self"
+ android:layout_marginTop="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_suitability_status"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="14sp"
+ tools:text="Transfer initiated by self." />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_transfer_reason"
+ android:layout_marginTop="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_transfer_initiated_by_self"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="14sp"
+ tools:text="Transfer reason." />
+
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/route_selection_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_transfer_reason"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="14sp"
+ tools:text="Selection button" />
+
</RelativeLayout>
</androidx.cardview.widget.CardView>
diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle
index 7f821c2..061c521 100644
--- a/samples/Support4Demos/build.gradle
+++ b/samples/Support4Demos/build.gradle
@@ -30,5 +30,6 @@
}
android {
+ compileSdkVersion 35
namespace "com.example.android.supportv4"
}
diff --git a/samples/SupportAnimationDemos/build.gradle b/samples/SupportAnimationDemos/build.gradle
index 5143dde..4288948 100644
--- a/samples/SupportAnimationDemos/build.gradle
+++ b/samples/SupportAnimationDemos/build.gradle
@@ -15,5 +15,6 @@
}
android {
+ compileSdk 35
namespace "com.example.android.support.animation"
}
diff --git a/samples/SupportContentDemos/build.gradle b/samples/SupportContentDemos/build.gradle
index d2ab9a4..c543967 100644
--- a/samples/SupportContentDemos/build.gradle
+++ b/samples/SupportContentDemos/build.gradle
@@ -31,5 +31,6 @@
}
android {
+ compileSdk 35
namespace "com.example.android.support.content.demos"
}
diff --git a/samples/SupportPreferenceDemos/build.gradle b/samples/SupportPreferenceDemos/build.gradle
index 73d482c..02d78ee 100644
--- a/samples/SupportPreferenceDemos/build.gradle
+++ b/samples/SupportPreferenceDemos/build.gradle
@@ -26,5 +26,6 @@
}
android {
+ compileSdk 35
namespace "com.example.androidx.preference"
-}
\ No newline at end of file
+}
diff --git a/samples/SupportRemoteCallbackDemos/build.gradle b/samples/SupportRemoteCallbackDemos/build.gradle
index 5864d41..7876ec4 100644
--- a/samples/SupportRemoteCallbackDemos/build.gradle
+++ b/samples/SupportRemoteCallbackDemos/build.gradle
@@ -27,6 +27,7 @@
}
android {
+ compileSdk 35
defaultConfig {
}
namespace "com.example.androidx.remotecallback.demos"
diff --git a/samples/SupportSliceDemos/build.gradle b/samples/SupportSliceDemos/build.gradle
index 007adcb..89d6061 100644
--- a/samples/SupportSliceDemos/build.gradle
+++ b/samples/SupportSliceDemos/build.gradle
@@ -32,6 +32,7 @@
}
android {
+ compileSdk 35
defaultConfig {
}
namespace "com.example.androidx.slice.demos"
diff --git a/samples/SupportTransitionDemos/build.gradle b/samples/SupportTransitionDemos/build.gradle
index 6119a42..eb68924 100644
--- a/samples/SupportTransitionDemos/build.gradle
+++ b/samples/SupportTransitionDemos/build.gradle
@@ -21,6 +21,7 @@
aaptOptions {
additionalParameters "--no-version-transitions"
}
+ compileSdk 35
namespace "com.example.android.support.transition"
}
diff --git a/samples/SupportWearDemos/build.gradle b/samples/SupportWearDemos/build.gradle
index 252fc05..3ae3154 100644
--- a/samples/SupportWearDemos/build.gradle
+++ b/samples/SupportWearDemos/build.gradle
@@ -27,6 +27,7 @@
}
android {
+ compileSdk 35
defaultConfig {
minSdkVersion 24
}
diff --git a/settings.gradle b/settings.gradle
index 8efe6fc..be19dab 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -96,7 +96,8 @@
value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
- publishing.onlyIf { it.authenticated }
+ // Do not publish scan for androidx-platform-dev
+ publishing.onlyIf { false }
}
}
@@ -1082,6 +1083,8 @@
File repoRoot = new File(rootDir, "../..").canonicalFile
includeOptionalProject(":xr:xr", new File(repoRoot, "xr/xr"), [BuildType.XR])
+includeOptionalProject(":xr:integration-tests:compose-adaptive-sample", new File(repoRoot, "xr/integration-tests/compose-adaptive-sample"), [BuildType.XR])
+includeOptionalProject(":xr:xr-material3-adaptive", new File(repoRoot, "xr/xr-material3-adaptive"), [BuildType.XR])
/////////////////////////////
//
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index 3bf9712..2e95f16 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -59,5 +59,6 @@
}
android {
+ compileSdk 35
namespace "androidx.slice.test"
}
diff --git a/startup/integration-tests/first-library/build.gradle b/startup/integration-tests/first-library/build.gradle
index 3acf247..ef0402b 100644
--- a/startup/integration-tests/first-library/build.gradle
+++ b/startup/integration-tests/first-library/build.gradle
@@ -35,5 +35,6 @@
}
android {
+ compileSdk 35
namespace "androidx.startup.first_library"
}
diff --git a/startup/integration-tests/second-library/build.gradle b/startup/integration-tests/second-library/build.gradle
index 68a15f7..96216cb 100644
--- a/startup/integration-tests/second-library/build.gradle
+++ b/startup/integration-tests/second-library/build.gradle
@@ -34,5 +34,6 @@
}
android {
+ compileSdk 35
namespace "androidx.startup.second_library"
}
diff --git a/startup/integration-tests/test-app/build.gradle b/startup/integration-tests/test-app/build.gradle
index 93f6d4d..f7893ef 100644
--- a/startup/integration-tests/test-app/build.gradle
+++ b/startup/integration-tests/test-app/build.gradle
@@ -21,6 +21,7 @@
}
android {
+ compileSdkVersion 35
buildTypes {
getByName("release") {
minifyEnabled = true
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index eed2f10..80f7aa2 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -50,5 +50,6 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.test.uiautomator.testapp"
}
diff --git a/testutils/testutils-appcompat/build.gradle b/testutils/testutils-appcompat/build.gradle
index 5c26d7f..611244d 100644
--- a/testutils/testutils-appcompat/build.gradle
+++ b/testutils/testutils-appcompat/build.gradle
@@ -41,6 +41,7 @@
}
android {
+ compileSdk 35
lintOptions {
disable "InvalidPackage" // Lint is unhappy about junit package
}
diff --git a/testutils/testutils-espresso/build.gradle b/testutils/testutils-espresso/build.gradle
index 87da559..0d449f2 100644
--- a/testutils/testutils-espresso/build.gradle
+++ b/testutils/testutils-espresso/build.gradle
@@ -38,6 +38,7 @@
}
android {
+ compileSdk 35
lintOptions {
disable "InvalidPackage" // Lint is unhappy about junit package
}
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index bc3fc1d..ff23ca5 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -42,5 +42,6 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.viewpager2.integration.testapp"
}
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 54fcef0..4eacf1f 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -21,6 +21,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
minSdk 28
}
diff --git a/wear/compose/compose-material3/integration-tests/build.gradle b/wear/compose/compose-material3/integration-tests/build.gradle
index 19bacd4..955f6fd 100644
--- a/wear/compose/compose-material3/integration-tests/build.gradle
+++ b/wear/compose/compose-material3/integration-tests/build.gradle
@@ -55,8 +55,9 @@
}
android {
+ compileSdk 35
defaultConfig {
minSdkVersion 25
}
namespace "androidx.wear.compose.material3.demos"
-}
\ No newline at end of file
+}
diff --git a/wear/compose/compose-material3/samples/build.gradle b/wear/compose/compose-material3/samples/build.gradle
index 0c87364..49df01d 100644
--- a/wear/compose/compose-material3/samples/build.gradle
+++ b/wear/compose/compose-material3/samples/build.gradle
@@ -45,6 +45,7 @@
}
android {
+ compileSdk 35
defaultConfig {
minSdkVersion 25
}
@@ -56,4 +57,4 @@
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Contains the sample code for the Android Wear Compose Material 3 Classes"
-}
\ No newline at end of file
+}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 0da615f..872bc9d 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index 96f05c4..117b475 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdkVersion 35
namespace "androidx.wear.compose.integration.macrobenchmark.target"
buildTypes {
@@ -53,4 +54,4 @@
implementation(project(path:':tracing:tracing-perfetto-binary'))
}
-android.defaultConfig.minSdkVersion 25
\ No newline at end of file
+android.defaultConfig.minSdkVersion 25
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index ca96d2b..b29b94ada 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
applicationId "androidx.wear.compose.integration.navigation"
minSdk 25
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index e72e167..23159f4a 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -52,14 +52,21 @@
package androidx.window.extensions.embedding {
public interface ActivityEmbeddingComponent {
+ method public default void clearActivityStackAttributesCalculator();
+ method public default void clearEmbeddedActivityWindowInfoCallback();
method public void clearSplitAttributesCalculator();
method public void clearSplitInfoCallback();
method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
method public default void invalidateTopVisibleSplitAttributes();
method public boolean isActivityEmbedded(android.app.Activity);
method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
@@ -67,12 +74,14 @@
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void unpinTopActivityStack(int);
method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
}
public class ActivityEmbeddingOptionsProperties {
field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
}
public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -92,6 +101,7 @@
public class ActivityStack {
method public java.util.List<android.app.Activity!> getActivities();
method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+ method public String? getTag();
method public boolean isEmpty();
}
@@ -102,6 +112,24 @@
field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
}
+ public final class ActivityStackAttributes {
+ method public android.graphics.Rect getRelativeBounds();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class ActivityStackAttributes.Builder {
+ ctor public ActivityStackAttributes.Builder();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public class ActivityStackAttributesCalculatorParams {
+ method public String getActivityStackTag();
+ method public android.os.Bundle getLaunchOptions();
+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+ }
+
public abstract class AnimationBackground {
method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
@@ -111,12 +139,68 @@
method @ColorInt public int getColor();
}
+ public final class AnimationParams {
+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method @AnimRes public int getChangeAnimationResId();
+ method @AnimRes public int getCloseAnimationResId();
+ method @AnimRes public int getOpenAnimationResId();
+ field @AnimRes public static final int DEFAULT_ANIMATION_RESOURCES_ID = -1; // 0xffffffff
+ }
+
+ public static final class AnimationParams.Builder {
+ ctor public AnimationParams.Builder();
+ method public androidx.window.extensions.embedding.AnimationParams build();
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setChangeAnimationResId(@AnimRes int);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setCloseAnimationResId(@AnimRes int);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setOpenAnimationResId(@AnimRes int);
+ }
+
+ public final class DividerAttributes {
+ method @ColorInt public int getDividerColor();
+ method public int getDividerType();
+ method public float getPrimaryMaxRatio();
+ method public float getPrimaryMinRatio();
+ method @Dimension public int getWidthDp();
+ method public boolean isDraggingToFullscreenAllowed();
+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Builder {
+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+ ctor public DividerAttributes.Builder(int);
+ method public androidx.window.extensions.embedding.DividerAttributes build();
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+ }
+
+ public class EmbeddedActivityWindowInfo {
+ method public android.app.Activity getActivity();
+ method public android.graphics.Rect getActivityStackBounds();
+ method public android.graphics.Rect getTaskBounds();
+ method public boolean isEmbedded();
+ }
+
public abstract class EmbeddingRule {
method public String? getTag();
}
+ public class ParentContainerInfo {
+ method public android.content.res.Configuration getConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+ method public android.view.WindowMetrics getWindowMetrics();
+ }
+
public class SplitAttributes {
- method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method @Deprecated public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method public androidx.window.extensions.embedding.AnimationParams getAnimationParams();
+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
@@ -124,8 +208,11 @@
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method @Deprecated public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.extensions.embedding.AnimationParams);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
@@ -260,6 +347,24 @@
method public android.graphics.Rect getBounds();
}
+ public final class DisplayFoldFeature {
+ method public int getType();
+ method public boolean hasProperties(int...);
+ method public boolean hasProperty(int);
+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+ field public static final int TYPE_HINGE = 1; // 0x1
+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DisplayFoldFeature.Builder {
+ ctor public DisplayFoldFeature.Builder(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature build();
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+ }
+
public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
ctor public FoldingFeature(android.graphics.Rect, int, int);
method public android.graphics.Rect getBounds();
@@ -271,9 +376,19 @@
field public static final int TYPE_HINGE = 2; // 0x2
}
+ public final class SupportedWindowFeatures {
+ method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+ }
+
+ public static final class SupportedWindowFeatures.Builder {
+ ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+ }
+
public interface WindowLayoutComponent {
method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
}
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 13940ea..e005796 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -52,14 +52,21 @@
package androidx.window.extensions.embedding {
public interface ActivityEmbeddingComponent {
+ method public default void clearActivityStackAttributesCalculator();
+ method public default void clearEmbeddedActivityWindowInfoCallback();
method public void clearSplitAttributesCalculator();
method public void clearSplitInfoCallback();
method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
method public default void invalidateTopVisibleSplitAttributes();
method public boolean isActivityEmbedded(android.app.Activity);
method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
@@ -67,12 +74,14 @@
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void unpinTopActivityStack(int);
method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
}
public class ActivityEmbeddingOptionsProperties {
field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
}
public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -92,6 +101,7 @@
public class ActivityStack {
method public java.util.List<android.app.Activity!> getActivities();
method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+ method public String? getTag();
method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.os.IBinder getToken();
method public boolean isEmpty();
}
@@ -103,6 +113,24 @@
field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
}
+ public final class ActivityStackAttributes {
+ method public android.graphics.Rect getRelativeBounds();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class ActivityStackAttributes.Builder {
+ ctor public ActivityStackAttributes.Builder();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public class ActivityStackAttributesCalculatorParams {
+ method public String getActivityStackTag();
+ method public android.os.Bundle getLaunchOptions();
+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+ }
+
public abstract class AnimationBackground {
method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
@@ -112,12 +140,68 @@
method @ColorInt public int getColor();
}
+ public final class AnimationParams {
+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method @AnimRes public int getChangeAnimationResId();
+ method @AnimRes public int getCloseAnimationResId();
+ method @AnimRes public int getOpenAnimationResId();
+ field @AnimRes public static final int DEFAULT_ANIMATION_RESOURCES_ID = -1; // 0xffffffff
+ }
+
+ public static final class AnimationParams.Builder {
+ ctor public AnimationParams.Builder();
+ method public androidx.window.extensions.embedding.AnimationParams build();
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setChangeAnimationResId(@AnimRes int);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setCloseAnimationResId(@AnimRes int);
+ method public androidx.window.extensions.embedding.AnimationParams.Builder setOpenAnimationResId(@AnimRes int);
+ }
+
+ public final class DividerAttributes {
+ method @ColorInt public int getDividerColor();
+ method public int getDividerType();
+ method public float getPrimaryMaxRatio();
+ method public float getPrimaryMinRatio();
+ method @Dimension public int getWidthDp();
+ method public boolean isDraggingToFullscreenAllowed();
+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Builder {
+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+ ctor public DividerAttributes.Builder(int);
+ method public androidx.window.extensions.embedding.DividerAttributes build();
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+ }
+
+ public class EmbeddedActivityWindowInfo {
+ method public android.app.Activity getActivity();
+ method public android.graphics.Rect getActivityStackBounds();
+ method public android.graphics.Rect getTaskBounds();
+ method public boolean isEmbedded();
+ }
+
public abstract class EmbeddingRule {
method public String? getTag();
}
+ public class ParentContainerInfo {
+ method public android.content.res.Configuration getConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+ method public android.view.WindowMetrics getWindowMetrics();
+ }
+
public class SplitAttributes {
- method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method @Deprecated public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method public androidx.window.extensions.embedding.AnimationParams getAnimationParams();
+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
@@ -125,8 +209,11 @@
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method @Deprecated public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.extensions.embedding.AnimationParams);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
@@ -261,6 +348,24 @@
method public android.graphics.Rect getBounds();
}
+ public final class DisplayFoldFeature {
+ method public int getType();
+ method public boolean hasProperties(int...);
+ method public boolean hasProperty(int);
+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+ field public static final int TYPE_HINGE = 1; // 0x1
+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DisplayFoldFeature.Builder {
+ ctor public DisplayFoldFeature.Builder(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature build();
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+ }
+
public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
ctor public FoldingFeature(android.graphics.Rect, int, int);
method public android.graphics.Rect getBounds();
@@ -272,9 +377,19 @@
field public static final int TYPE_HINGE = 2; // 0x2
}
+ public final class SupportedWindowFeatures {
+ method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+ }
+
+ public static final class SupportedWindowFeatures.Builder {
+ ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+ }
+
public interface WindowLayoutComponent {
method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
}
diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
new file mode 100644
index 0000000..953ace1
--- /dev/null
+++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import org.junit.Test;
+
+/**
+ * Verifies {@link ActivityStackAttributes} behavior.
+ */
+public class ActivityStackAttributesTest {
+
+ @Test
+ public void testActivityStackAttributesDefaults() {
+ final ActivityStackAttributes defaultAttrs = new ActivityStackAttributes.Builder().build();
+ assertThat(defaultAttrs.getRelativeBounds().isEmpty()).isTrue();
+ assertThat(defaultAttrs.getWindowAttributes().getDimAreaBehavior())
+ .isEqualTo(DIM_AREA_ON_ACTIVITY_STACK);
+ }
+
+ @Test
+ public void testActivityStackAttributesEqualsMatchHashCode() {
+ final ActivityStackAttributes attrs1 = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(0, 0, 10, 10))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+ .build();
+
+ final ActivityStackAttributes attrs2 = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(0, 0, 10, 10))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+ .build();
+
+ final ActivityStackAttributes attrs3 = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(10, 0, 20, 10))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+ .build();
+
+ final ActivityStackAttributes attrs4 = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(10, 0, 20, 10))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+ .build();
+
+ final ActivityStackAttributes attrs5 = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(0, 0, 10, 10))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+ .build();
+
+ assertThat(attrs1).isNotEqualTo(attrs2);
+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs2.hashCode());
+ assertThat(attrs1).isNotEqualTo(attrs3);
+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs3.hashCode());
+ assertThat(attrs1).isNotEqualTo(attrs4);
+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs4.hashCode());
+ assertThat(attrs1).isEqualTo(attrs5);
+ assertThat(attrs1.hashCode()).isEqualTo(attrs5.hashCode());
+ }
+}
diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
new file mode 100644
index 0000000..f699b61
--- /dev/null
+++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link EmbeddedActivityWindowInfo} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmbeddedActivityWindowInfoTest {
+
+ @Mock
+ private Activity mActivity;
+ @Mock
+ private Activity mActivity2;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testGetter() {
+ final Rect taskBounds = new Rect(0, 0, 1000, 2000);
+ final Rect activityStackBounds = new Rect(0, 0, 1000, 1000);
+ final EmbeddedActivityWindowInfo info = new EmbeddedActivityWindowInfo(mActivity,
+ true /* isEmbedded */, taskBounds, activityStackBounds);
+
+ assertEquals(mActivity, info.getActivity());
+ assertTrue(info.isEmbedded());
+ assertEquals(taskBounds, info.getTaskBounds());
+ assertEquals(activityStackBounds, info.getActivityStackBounds());
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ final EmbeddedActivityWindowInfo info1 = new EmbeddedActivityWindowInfo(mActivity,
+ true /* isEmbedded */,
+ new Rect(0, 0, 1000, 2000),
+ new Rect(0, 0, 1000, 1000));
+ final EmbeddedActivityWindowInfo info2 = new EmbeddedActivityWindowInfo(mActivity2,
+ true /* isEmbedded */,
+ new Rect(0, 0, 1000, 2000),
+ new Rect(0, 0, 1000, 1000));
+ final EmbeddedActivityWindowInfo info3 = new EmbeddedActivityWindowInfo(mActivity,
+ false /* isEmbedded */,
+ new Rect(0, 0, 1000, 2000),
+ new Rect(0, 0, 1000, 1000));
+ final EmbeddedActivityWindowInfo info4 = new EmbeddedActivityWindowInfo(mActivity,
+ true /* isEmbedded */,
+ new Rect(0, 0, 1000, 1000),
+ new Rect(0, 0, 1000, 1000));
+ final EmbeddedActivityWindowInfo info5 = new EmbeddedActivityWindowInfo(mActivity,
+ true /* isEmbedded */,
+ new Rect(0, 0, 1000, 2000),
+ new Rect(0, 0, 1000, 1500));
+ final EmbeddedActivityWindowInfo info6 = new EmbeddedActivityWindowInfo(mActivity,
+ true /* isEmbedded */,
+ new Rect(0, 0, 1000, 2000),
+ new Rect(0, 0, 1000, 1000));
+
+ assertNotEquals(info1, info2);
+ assertNotEquals(info1, info3);
+ assertNotEquals(info1, info4);
+ assertNotEquals(info1, info5);
+ assertEquals(info1, info6);
+
+ assertNotEquals(info1.hashCode(), info2.hashCode());
+ assertNotEquals(info1.hashCode(), info3.hashCode());
+ assertNotEquals(info1.hashCode(), info4.hashCode());
+ assertNotEquals(info1.hashCode(), info5.hashCode());
+ assertEquals(info1.hashCode(), info6.hashCode());
+ }
+}
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
index 6d01778..cb53d62 100644
--- 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
@@ -158,6 +158,8 @@
void startRearDisplaySession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> consumer);
+ // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
+ // the latest library.
/**
* Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
* provided in the {@code startRearDisplaySession} method. This method is only
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index d032b00..ef79b80 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -22,6 +22,8 @@
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.window.extensions.RequiresVendorApiLevel;
import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.core.util.function.Consumer;
@@ -42,6 +44,12 @@
public interface ActivityEmbeddingComponent {
/**
+ * The vendor API level of the overlay feature APIs.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ int OVERLAY_FEATURE_API_LEVEL = 8;
+
+ /**
* Updates the rules of embedding activities that are started in the client process.
*/
@RequiresVendorApiLevel(level = 1)
@@ -253,6 +261,93 @@
}
/**
+ * Returns the {@link ParentContainerInfo} by the {@link ActivityStack} token, or {@code null}
+ * if there's not such {@link ActivityStack} associated with the {@code token}.
+ *
+ * @param activityStackToken the token of an {@link ActivityStack}.
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ @Nullable
+ default ParentContainerInfo getParentContainerInfo(
+ @NonNull ActivityStack.Token activityStackToken) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Sets a function to compute the {@link ActivityStackAttributes} for the ActivityStack given
+ * for the current window and device state provided in
+ * {@link ActivityStackAttributesCalculatorParams} on the main thread.
+ * <p>
+ * This calculator function is only triggered if the {@link ActivityStack#getTag()} is
+ * specified. Similar to {@link #setSplitAttributesCalculator(Function)}, the calculator
+ * function could be triggered multiple times. It will be triggered whenever there's a
+ * launching standalone {@link ActivityStack} with {@link ActivityStack#getTag()} specified,
+ * or a parent window or device state update, such as device rotation, folding state change,
+ * or the host task goes to multi-window mode.
+ *
+ * @param calculator The calculator function to calculate {@link ActivityStackAttributes} based
+ * on {@link ActivityStackAttributesCalculatorParams}.
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ default void setActivityStackAttributesCalculator(@NonNull Function<
+ ActivityStackAttributesCalculatorParams, ActivityStackAttributes> calculator) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Clears the calculator function previously set by
+ * {@link #setActivityStackAttributesCalculator(Function)}
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ default void clearActivityStackAttributesCalculator() {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Updates {@link ActivityStackAttributes} to an {@link ActivityStack} specified with
+ * {@code token} and applies the change directly. If there's no such an {@link ActivityStack},
+ * this method is no-op.
+ *
+ * @param token The {@link ActivityStack} to update.
+ * @param activityStackAttributes The attributes to be applied
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ default void updateActivityStackAttributes(@NonNull ActivityStack.Token token,
+ @NonNull ActivityStackAttributes activityStackAttributes) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Gets the {@link ActivityStack}'s token by {@code tag}, or {@code null} if there's no
+ * {@link ActivityStack} associated with the {@code tag}. For example, the {@link ActivityStack}
+ * is dismissed before the is method is called.
+ * <p>
+ * The {@link ActivityStack} token can be obtained immediately after the {@link ActivityStack}
+ * is created. This method is usually used when Activity Embedding library wants to
+ * {@link #updateActivityStackAttributes} before receiving
+ * the {@link ActivityStack} record from the callback set by
+ * {@link #registerActivityStackCallback}.
+ * <p>
+ * For example, an app launches an overlay container and calls
+ * {@link #updateActivityStackAttributes} immediately right before the overlay
+ * {@link ActivityStack} is received from {@link #registerActivityStackCallback}.
+ *
+ * @param tag A unique identifier of an {@link ActivityStack} if set
+ * @return The {@link ActivityStack}'s token that the tag is associated with, or {@code null}
+ * if there's no such an {@link ActivityStack}.
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ @Nullable
+ default ActivityStack.Token getActivityStackToken(@NonNull String tag) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
* Registers a callback that notifies WindowManager Jetpack about changes in
* {@link ActivityStack}.
* <p>
@@ -309,4 +404,48 @@
throw new UnsupportedOperationException("This method must not be called unless there is a"
+ " corresponding override implementation on the device.");
}
+
+ /**
+ * Sets a callback that notifies WindowManager Jetpack about changes for a given
+ * {@link Activity} to its {@link EmbeddedActivityWindowInfo}.
+ * <p>
+ * The callback will be invoked when the {@link EmbeddedActivityWindowInfo} is changed after
+ * the {@link Activity} is launched. Similar to {@link Activity#onConfigurationChanged}, the
+ * callback will only be invoked for visible {@link Activity}.
+ *
+ * @param executor the executor to dispatch {@link EmbeddedActivityWindowInfo} change.
+ * @param callback the callback to notify {@link EmbeddedActivityWindowInfo} change.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ default void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor,
+ @NonNull Consumer<EmbeddedActivityWindowInfo> callback) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Clears the callback previously set by
+ * {@link #setEmbeddedActivityWindowInfoCallback(Executor, Consumer)}
+ */
+ @RequiresVendorApiLevel(level = 6)
+ default void clearEmbeddedActivityWindowInfoCallback() {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Returns the {@link EmbeddedActivityWindowInfo} of the given {@link Activity}, or
+ * {@code null} if the {@link Activity} is not attached.
+ * <p>
+ * This API can be used when {@link #setEmbeddedActivityWindowInfoCallback} is not set before
+ * the Activity is attached.
+ *
+ * @param activity the {@link Activity} to get {@link EmbeddedActivityWindowInfo} for.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @Nullable
+ default EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
index 5279caa..fce31d8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
@@ -29,6 +29,17 @@
private ActivityEmbeddingOptionsProperties() {}
/**
+ * The key of the unique identifier that put into {@link android.app.ActivityOptions}.
+ * <p>
+ * Type: {@link android.os.Bundle#putString(String, String) String}
+ * <p>
+ * An {@code OverlayCreateParams} property that represents the unique identifier of the overlay
+ * container.
+ */
+ public static final String KEY_OVERLAY_TAG =
+ "androidx.window.extensions.embedding.OverlayTag";
+
+ /**
* The key of {@link ActivityStack.Token#toBundle()} that put into
* {@link android.app.ActivityOptions}.
* <p>
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 69b7ad3..0441d37 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
@@ -16,12 +16,15 @@
package androidx.window.extensions.embedding;
+import static androidx.window.extensions.embedding.ActivityEmbeddingComponent.OVERLAY_FEATURE_API_LEVEL;
+
import android.app.Activity;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.window.extensions.RequiresVendorApiLevel;
@@ -43,6 +46,9 @@
@NonNull
private final Token mToken;
+ @Nullable
+ private final String mTag;
+
/**
* The {@code ActivityStack} constructor
*
@@ -51,14 +57,18 @@
* @param isEmpty Indicates whether there's any {@link Activity} running in this
* {@code ActivityStack}
* @param token The token to identify this {@code ActivityStack}
+ * @param tag A unique identifier of {@link ActivityStack}. Only specifies for the overlay
+ * standalone {@link ActivityStack} currently.
*/
- ActivityStack(@NonNull List<Activity> activities, boolean isEmpty, @NonNull Token token) {
+ ActivityStack(@NonNull List<Activity> activities, boolean isEmpty, @NonNull Token token,
+ @Nullable String tag) {
Objects.requireNonNull(activities);
Objects.requireNonNull(token);
mActivities = new ArrayList<>(activities);
mIsEmpty = isEmpty;
mToken = token;
+ mTag = tag;
}
/**
@@ -110,6 +120,15 @@
return mToken.getRawToken();
}
+ /**
+ * Returns the associated tag if specified. Otherwise, returns {@code null}.
+ */
+ @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+ @Nullable
+ public String getTag() {
+ return mTag;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -117,7 +136,8 @@
ActivityStack that = (ActivityStack) o;
return mActivities.equals(that.mActivities)
&& mIsEmpty == that.mIsEmpty
- && mToken.equals(that.mToken);
+ && mToken.equals(that.mToken)
+ && Objects.equals(mTag, that.mTag);
}
@Override
@@ -125,6 +145,7 @@
int result = (mIsEmpty ? 1 : 0);
result = result * 31 + mActivities.hashCode();
result = result * 31 + mToken.hashCode();
+ result = result * 31 + Objects.hashCode(mTag);
return result;
}
@@ -135,6 +156,7 @@
return "ActivityStack{" + "mActivities=" + mActivities
+ ", mIsEmpty=" + mIsEmpty
+ ", mToken=" + mToken
+ + ", mTag=" + mTag
+ '}';
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
new file mode 100644
index 0000000..eb4f743
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+/**
+ * Attributes used to update the layout and configuration of an {@link ActivityStack}.
+ */
+public final class ActivityStackAttributes {
+
+ @NonNull
+ private final Rect mRelativeBounds;
+
+ @NonNull
+ private final WindowAttributes mWindowAttributes;
+
+ private ActivityStackAttributes(@NonNull Rect relativeBounds,
+ @NonNull WindowAttributes windowAttributes) {
+ mRelativeBounds = relativeBounds;
+ mWindowAttributes = windowAttributes;
+ }
+
+ /**
+ * Returns the requested bounds of an {@link ActivityStack} which relative to its parent
+ * container.
+ * <p>
+ * {@link Rect#isEmpty() Empty} bounds mean that this {@link ActivityStack} should fill its
+ * parent container bounds.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Rect getRelativeBounds() {
+ return mRelativeBounds;
+ }
+
+ /**
+ * Returns the {@link WindowAttributes} which contains the configurations of the embedded
+ * Activity windows with this attributes.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public WindowAttributes getWindowAttributes() {
+ return mWindowAttributes;
+ }
+
+ @Override
+ public int hashCode() {
+ return mRelativeBounds.hashCode() * 31 + mWindowAttributes.hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof ActivityStackAttributes)) return false;
+ final ActivityStackAttributes attrs = (ActivityStackAttributes) obj;
+ return mRelativeBounds.equals(attrs.mRelativeBounds)
+ && mWindowAttributes.equals(attrs.mWindowAttributes);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return ActivityStackAttributes.class.getSimpleName() + ": {"
+ + " relativeBounds=" + mRelativeBounds
+ + ", windowAttributes=" + mWindowAttributes
+ + "}";
+ }
+
+ /** The builder class of {@link ActivityStackAttributes}. */
+ public static final class Builder {
+
+ /** The {@link ActivityStackAttributes} builder constructor. */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder() {}
+
+ @NonNull
+ private final Rect mRelativeBounds = new Rect();
+
+ @NonNull
+ private WindowAttributes mWindowAttributes =
+ new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK);
+
+ /**
+ * Sets the requested relative bounds of the {@link ActivityStack}. If this value is
+ * not specified, {@link #getRelativeBounds()} defaults to {@link Rect#isEmpty() empty}
+ * bounds, which means to follow the parent container bounds.
+ *
+ * @param relativeBounds The requested relative bounds.
+ * @return This {@code Builder}.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setRelativeBounds(@NonNull Rect relativeBounds) {
+ mRelativeBounds.set(relativeBounds);
+ return this;
+ }
+
+ /**
+ * Sets the window attributes. If this value is not specified, the
+ * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the
+ * {@link ActivityStack} of the requested activity.
+ *
+ * @param attributes The {@link WindowAttributes}
+ * @return This {@code Builder}.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public Builder setWindowAttributes(@NonNull WindowAttributes attributes) {
+ mWindowAttributes = attributes;
+ return this;
+ }
+
+ /**
+ * Builds an {@link ActivityStackAttributes} instance.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public ActivityStackAttributes build() {
+ return new ActivityStackAttributes(mRelativeBounds, mWindowAttributes);
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
new file mode 100644
index 0000000..99afdc6
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.core.util.function.Function;
+
+/**
+ * The parameter container used in standalone {@link ActivityStack} calculator function to report
+ * {@link ParentContainerInfo} and associated {@link ActivityStack#getTag()} to calculate
+ * {@link ActivityStackAttributes} when there's a parent container information update or a
+ * standalone {@link ActivityStack} is going to be launched.
+ *
+ * @see ActivityEmbeddingComponent#setActivityStackAttributesCalculator(Function)
+ */
+public class ActivityStackAttributesCalculatorParams {
+
+ @NonNull
+ private final ParentContainerInfo mParentContainerInfo;
+
+ @NonNull
+ private final String mActivityStackTag;
+
+ @NonNull
+ private final Bundle mLaunchOptions;
+
+ /**
+ * {@code ActivityStackAttributesCalculatorParams} constructor.
+ *
+ * @param parentContainerInfo The {@link ParentContainerInfo} of the standalone
+ * {@link ActivityStack} to apply the
+ * {@link ActivityStackAttributes}.
+ * @param activityStackTag The unique identifier of {@link ActivityStack} to apply the
+ * {@link ActivityStackAttributes}.
+ * @param launchOptions The options to launch the {@link ActivityStack}.
+ */
+ ActivityStackAttributesCalculatorParams(@NonNull ParentContainerInfo parentContainerInfo,
+ @NonNull String activityStackTag, @NonNull Bundle launchOptions) {
+ mParentContainerInfo = parentContainerInfo;
+ mActivityStackTag = activityStackTag;
+ mLaunchOptions = launchOptions;
+ }
+
+ /**
+ * Returns {@link ParentContainerInfo} of the standalone {@link ActivityStack} to calculate.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public ParentContainerInfo getParentContainerInfo() {
+ return mParentContainerInfo;
+ }
+
+ /**
+ * Returns unique identifier of the standalone {@link ActivityStack} to calculate.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public String getActivityStackTag() {
+ return mActivityStackTag;
+ }
+
+ /**
+ * Returns options that passed from WM Jetpack to WM Extensions library to launch an
+ * {@link ActivityStack}. {@link Bundle#isEmpty() empty} options mean there's no launch options.
+ * <p>
+ * For example, an {@link ActivityStack} launch options could be an
+ * {@link android.app.ActivityOptions} bundle that contains information to build an overlay
+ * {@link ActivityStack}.
+ * <p>
+ * The launch options will be used for initializing standalone {@link ActivityStack} with
+ * {@link #getActivityStackTag()} specified. The logic is owned by WM Jetpack, which is usually
+ * from the {@link android.app.ActivityOptions}, WM Extensions library must not touch the
+ * options.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Bundle getLaunchOptions() {
+ return mLaunchOptions;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return ActivityStackAttributesCalculatorParams.class.getSimpleName() + ":{"
+ + "parentContainerInfo=" + mParentContainerInfo
+ + "activityStackTag=" + mActivityStackTag
+ + "launchOptions=" + mLaunchOptions
+ + "}";
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java
new file mode 100644
index 0000000..53aedbb
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2024 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.embedding;
+
+import android.content.res.Resources;
+
+import androidx.annotation.AnimRes;
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.Objects;
+
+/**
+ * A class to represent the animation parameters to use while animating embedding activity
+ * containers.
+ *
+ * @see SplitAttributes.Builder#setAnimationParams
+ */
+public final class AnimationParams {
+
+ /**
+ * The default value for animation resources ID, which means to use the system default
+ * animation.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @SuppressWarnings("ResourceType") // Use as a hint to use the system default animation.
+ @AnimRes
+ public static final int DEFAULT_ANIMATION_RESOURCES_ID = 0xFFFFFFFF;
+
+ @NonNull
+ private final AnimationBackground mAnimationBackground;
+
+ @AnimRes
+ private final int mOpenAnimationResId;
+
+ @AnimRes
+ private final int mCloseAnimationResId;
+
+ @AnimRes
+ private final int mChangeAnimationResId;
+
+ /**
+ * Creates an instance of this {@code AnimationParams}.
+ *
+ * @param animationBackground The {@link AnimationBackground} to use for the animation
+ * involving this {@code AnimationParams} object.
+ * @param openAnimationResId The animation resources ID from the "android" package to use for
+ * open transitions.
+ * @param closeAnimationResId The animation resources ID from the "android" package to use for
+ * close transitions.
+ * @param changeAnimationResId The animation resources ID from the "android" package to use for
+ * change (resize or move) transitions.
+ */
+ private AnimationParams(@NonNull AnimationBackground animationBackground,
+ @AnimRes int openAnimationResId, @AnimRes int closeAnimationResId,
+ @AnimRes int changeAnimationResId) {
+ mAnimationBackground = animationBackground;
+ mOpenAnimationResId = openAnimationResId;
+ mCloseAnimationResId = closeAnimationResId;
+ mChangeAnimationResId = changeAnimationResId;
+ }
+
+ /**
+ * Returns the {@link AnimationBackground} to use for the background during the animation.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationBackground getAnimationBackground() {
+ return mAnimationBackground;
+ }
+
+ /**
+ * Gets the open animation.
+ *
+ * @return the open animation transition resources ID from the "android" package.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @AnimRes
+ public int getOpenAnimationResId() {
+ return mOpenAnimationResId;
+ }
+
+ /**
+ * Gets the close animation.
+ *
+ * @return the close animation transition resources ID from the "android" package.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @AnimRes
+ public int getCloseAnimationResId() {
+ return mCloseAnimationResId;
+ }
+
+ /**
+ * Gets the change (resize or move) animation.
+ *
+ * @return the change (resize or move) animation transition resources ID from the "android"
+ * package.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @AnimRes
+ public int getChangeAnimationResId() {
+ return mChangeAnimationResId;
+ }
+
+ /**
+ * Builder for creating an instance of {@link AnimationParams}.
+ *
+ * - The default animation background is to use the current theme window background color.
+ * - The default animation resources ID's for transitions is the system default.
+ */
+ public static final class Builder {
+ @NonNull
+ private AnimationBackground mAnimationBackground =
+ AnimationBackground.ANIMATION_BACKGROUND_DEFAULT;
+
+ @AnimRes
+ private int mOpenAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+ @AnimRes
+ private int mCloseAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+ @AnimRes
+ private int mChangeAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+ /** Creates a new {@link AnimationParams.Builder} to create {@link AnimationParams}. */
+ @RequiresVendorApiLevel(level = 7)
+ public Builder() {
+ }
+
+ /**
+ * Sets the {@link AnimationBackground} to use for the background during the animation.
+ * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which
+ * means to use the current theme window background color.
+ *
+ * @param background An {@link AnimationBackground} to be used for the animation.
+ * @return this {@code Builder}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationParams.Builder setAnimationBackground(
+ @NonNull AnimationBackground background) {
+ mAnimationBackground = background;
+ return this;
+ }
+
+ /**
+ * Sets the open animation.
+ * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+ * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+ *
+ * @param resId The resources ID to set from the "android" package.
+ * @return this {@code Builder}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationParams.Builder setOpenAnimationResId(@AnimRes int resId) {
+ mOpenAnimationResId = resId;
+ return this;
+ }
+
+ /**
+ * Sets the close animation.
+ * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+ * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+ *
+ * @param resId The resources ID to set from the "android" package.
+ * @return this {@code Builder}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationParams.Builder setCloseAnimationResId(@AnimRes int resId) {
+ mCloseAnimationResId = resId;
+ return this;
+ }
+
+ /**
+ * Sets the change (resize or move) animation.
+ * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+ * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+ *
+ * @param resId The resources ID to set from the "android" package.
+ * @return this {@code Builder}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationParams.Builder setChangeAnimationResId(@AnimRes int resId) {
+ mChangeAnimationResId = resId;
+ return this;
+ }
+
+ /**
+ * Builds a {@link AnimationParams} instance with the attributes
+ * specified by {@link #setAnimationBackground(AnimationBackground)},
+ * {@link #setOpenAnimationResId(int)}, {@link #setCloseAnimationResId(int)},
+ * and {@link #setChangeAnimationResId(int)}.
+ *
+ * @return the new {@code AnimationParams} instance.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public AnimationParams build() {
+ return new AnimationParams(mAnimationBackground, mOpenAnimationResId,
+ mCloseAnimationResId, mChangeAnimationResId);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AnimationParams)) return false;
+ AnimationParams that = (AnimationParams) o;
+ return mAnimationBackground.equals(that.mAnimationBackground)
+ && mOpenAnimationResId == that.mOpenAnimationResId
+ && mCloseAnimationResId == that.mCloseAnimationResId
+ && mChangeAnimationResId == that.mChangeAnimationResId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAnimationBackground, mOpenAnimationResId, mCloseAnimationResId,
+ mChangeAnimationResId);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return AnimationParams.class.getSimpleName() + "{"
+ + "animationBackground=" + mAnimationBackground
+ + ", openAnimation=" + mOpenAnimationResId
+ + ", closeAnimation=" + mCloseAnimationResId
+ + ", changeAnimation=" + mChangeAnimationResId
+ + "}";
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
new file mode 100644
index 0000000..f38daff
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2024 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.embedding;
+
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The attributes of the divider layout and behavior.
+ *
+ * @see SplitAttributes.Builder#setDividerAttributes(DividerAttributes)
+ */
+public final class DividerAttributes {
+
+ /**
+ * A divider type that draws a static line between the primary and secondary containers.
+ */
+ public static final int DIVIDER_TYPE_FIXED = 1;
+
+ /**
+ * A divider type that draws a line between the primary and secondary containers with a drag
+ * handle that the user can drag and resize the containers.
+ */
+ public static final int DIVIDER_TYPE_DRAGGABLE = 2;
+
+ @IntDef({DIVIDER_TYPE_FIXED, DIVIDER_TYPE_DRAGGABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface DividerType {
+ }
+
+ /**
+ * A special value to indicate that the ratio is unset. which means the system will choose a
+ * default value based on the display size and form factor.
+ *
+ * @see #getPrimaryMinRatio()
+ * @see #getPrimaryMaxRatio()
+ */
+ public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+
+ /**
+ * A special value to indicate that the width is unset. which means the system will choose a
+ * default value based on the display size and form factor.
+ *
+ * @see #getWidthDp()
+ */
+ public static final int WIDTH_SYSTEM_DEFAULT = -1;
+
+ /** The {@link DividerType}. */
+ private final @DividerType int mDividerType;
+
+ /**
+ * The divider width in dp. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the system
+ * will choose a default value based on the display size and form factor.
+ */
+ private final @Dimension int mWidthDp;
+
+ /**
+ * The min split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
+ * the system will choose a default value based on the display size and form factor. Will only
+ * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the secondary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ private final float mPrimaryMinRatio;
+
+ /**
+ * The max split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
+ * the system will choose a default value based on the display size and form factor. Will only
+ * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the primary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ private final float mPrimaryMaxRatio;
+
+ /** The color of the divider. */
+ private final @ColorInt int mDividerColor;
+
+ /** Whether it is allowed to expand a container to full screen by dragging the divider. */
+ private final boolean mIsDraggingToFullscreenAllowed;
+
+ /**
+ * Constructor of {@link DividerAttributes}.
+ *
+ * @param dividerType the divider type. See {@link DividerType}.
+ * @param widthDp the width of the divider.
+ * @param primaryMinRatio the min split ratio for the primary container.
+ * @param primaryMaxRatio the max split ratio for the primary container.
+ * @param dividerColor the color of the divider.
+ * @param isDraggingToFullscreenAllowed whether it is allowed to expand a container to full
+ * screen by dragging the divider.
+ * @throws IllegalStateException if the provided values are invalid.
+ */
+ private DividerAttributes(
+ @DividerType int dividerType,
+ @Dimension int widthDp,
+ float primaryMinRatio,
+ float primaryMaxRatio,
+ @ColorInt int dividerColor,
+ boolean isDraggingToFullscreenAllowed) {
+ if (dividerType == DIVIDER_TYPE_FIXED
+ && (primaryMinRatio != RATIO_SYSTEM_DEFAULT
+ || primaryMaxRatio != RATIO_SYSTEM_DEFAULT)) {
+ throw new IllegalStateException(
+ "primaryMinRatio and primaryMaxRatio must be RATIO_SYSTEM_DEFAULT for "
+ + "DIVIDER_TYPE_FIXED.");
+ }
+ if (primaryMinRatio != RATIO_SYSTEM_DEFAULT && primaryMaxRatio != RATIO_SYSTEM_DEFAULT
+ && primaryMinRatio > primaryMaxRatio) {
+ throw new IllegalStateException(
+ "primaryMinRatio must be less than or equal to primaryMaxRatio");
+ }
+ mDividerType = dividerType;
+ mWidthDp = widthDp;
+ mPrimaryMinRatio = primaryMinRatio;
+ mPrimaryMaxRatio = primaryMaxRatio;
+ mDividerColor = dividerColor;
+ mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
+ }
+
+ /**
+ * Returns the divider type.
+ *
+ * @see #DIVIDER_TYPE_FIXED
+ * @see #DIVIDER_TYPE_DRAGGABLE
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public @DividerType int getDividerType() {
+ return mDividerType;
+ }
+
+ /**
+ * Returns the width of the divider. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means
+ * the system will choose a default value based on the display size and form factor.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public @Dimension int getWidthDp() {
+ return mWidthDp;
+ }
+
+ /**
+ * Returns the min split ratio for the primary container the divider can be dragged to. It
+ * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+ * based on the display size and form factor. Will only be used when the divider type is
+ * {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the secondary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public float getPrimaryMinRatio() {
+ return mPrimaryMinRatio;
+ }
+
+ /**
+ * Returns the max split ratio for the primary container the divider can be dragged to. It
+ * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+ * based on the display size and form factor. Will only be used when the divider type is
+ * {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the primary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public float getPrimaryMaxRatio() {
+ return mPrimaryMaxRatio;
+ }
+
+ /** Returns the color of the divider. */
+ @RequiresVendorApiLevel(level = 6)
+ public @ColorInt int getDividerColor() {
+ return mDividerColor;
+ }
+
+ /**
+ * Returns whether it is allowed to expand a container to full screen by dragging the
+ * divider. Default is {@code true}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ public boolean isDraggingToFullscreenAllowed() {
+ return mIsDraggingToFullscreenAllowed;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof DividerAttributes)) return false;
+ final DividerAttributes other = (DividerAttributes) obj;
+ return mDividerType == other.mDividerType
+ && mWidthDp == other.mWidthDp
+ && mPrimaryMinRatio == other.mPrimaryMinRatio
+ && mPrimaryMaxRatio == other.mPrimaryMaxRatio
+ && mDividerColor == other.mDividerColor
+ && mIsDraggingToFullscreenAllowed == other.mIsDraggingToFullscreenAllowed;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDividerType, mWidthDp, mPrimaryMinRatio, mPrimaryMaxRatio,
+ mIsDraggingToFullscreenAllowed);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return DividerAttributes.class.getSimpleName() + "{"
+ + "dividerType=" + mDividerType
+ + ", width=" + mWidthDp
+ + ", minPrimaryRatio=" + mPrimaryMinRatio
+ + ", maxPrimaryRatio=" + mPrimaryMaxRatio
+ + ", dividerColor=" + mDividerColor
+ + ", isDraggingToFullscreenAllowed=" + mIsDraggingToFullscreenAllowed
+ + "}";
+ }
+
+ /** The {@link DividerAttributes} builder. */
+ public static final class Builder {
+
+ private final @DividerType int mDividerType;
+
+ private @Dimension int mWidthDp = WIDTH_SYSTEM_DEFAULT;
+
+ private float mPrimaryMinRatio = RATIO_SYSTEM_DEFAULT;
+
+ private float mPrimaryMaxRatio = RATIO_SYSTEM_DEFAULT;
+
+ private @ColorInt int mDividerColor = Color.BLACK;
+
+ private boolean mIsDraggingToFullscreenAllowed = false;
+
+ /**
+ * The {@link DividerAttributes} builder constructor.
+ *
+ * @param dividerType the divider type, possible values are {@link #DIVIDER_TYPE_FIXED} and
+ * {@link #DIVIDER_TYPE_DRAGGABLE}.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder(@DividerType int dividerType) {
+ mDividerType = dividerType;
+ }
+
+ /**
+ * The {@link DividerAttributes} builder constructor initialized by an existing
+ * {@link DividerAttributes}.
+ *
+ * @param original the original {@link DividerAttributes} to initialize the {@link Builder}.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder(@NonNull DividerAttributes original) {
+ Objects.requireNonNull(original);
+ mDividerType = original.mDividerType;
+ mWidthDp = original.mWidthDp;
+ mPrimaryMinRatio = original.mPrimaryMinRatio;
+ mPrimaryMaxRatio = original.mPrimaryMaxRatio;
+ mDividerColor = original.mDividerColor;
+ mIsDraggingToFullscreenAllowed = original.mIsDraggingToFullscreenAllowed;
+ }
+
+ /**
+ * Sets the divider width. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the
+ * system will choose a default value based on the display size and form factor.
+ *
+ * @throws IllegalArgumentException if the provided value is invalid.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setWidthDp(@Dimension int widthDp) {
+ if (widthDp != WIDTH_SYSTEM_DEFAULT && widthDp < 0) {
+ throw new IllegalArgumentException(
+ "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT.");
+ }
+ mWidthDp = widthDp;
+ return this;
+ }
+
+ /**
+ * Sets the min split ratio for the primary container. It defaults to
+ * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value based
+ * on the display size and form factor. Will only be used when the divider type is
+ * {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the secondary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @param primaryMinRatio the min ratio for the primary container. Must be in range
+ * [0.0, 1.0) or {@link #RATIO_SYSTEM_DEFAULT}.
+ * @throws IllegalArgumentException if the provided value is invalid.
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setPrimaryMinRatio(float primaryMinRatio) {
+ if (primaryMinRatio != RATIO_SYSTEM_DEFAULT
+ && (primaryMinRatio >= 1.0 || primaryMinRatio < 0.0)) {
+ throw new IllegalArgumentException(
+ "primaryMinRatio must be in [0.0, 1.0) or RATIO_SYSTEM_DEFAULT.");
+ }
+ mPrimaryMinRatio = primaryMinRatio;
+ return this;
+ }
+
+ /**
+ * Sets the max split ratio for the primary container. It defaults to
+ * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+ * based on the display size and form factor. Will only be used when the divider type is
+ * {@link #DIVIDER_TYPE_DRAGGABLE}.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+ * beyond this ratio, and when dragging is finished, the system will choose to either fully
+ * expand the primary container or move the divider back to this ratio.
+ *
+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+ * drag beyond this ratio.
+ *
+ * @param primaryMaxRatio the max ratio for the primary container. Must be in range
+ * (0.0, 1.0] or {@link #RATIO_SYSTEM_DEFAULT}.
+ * @throws IllegalArgumentException if the provided value is invalid.
+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setPrimaryMaxRatio(float primaryMaxRatio) {
+ if (primaryMaxRatio != RATIO_SYSTEM_DEFAULT
+ && (primaryMaxRatio > 1.0 || primaryMaxRatio <= 0.0)) {
+ throw new IllegalArgumentException(
+ "primaryMaxRatio must be in (0.0, 1.0] or RATIO_SYSTEM_DEFAULT.");
+ }
+ mPrimaryMaxRatio = primaryMaxRatio;
+ return this;
+ }
+
+ /**
+ * Sets the color of the divider. If not set, the default color {@link Color#BLACK} is
+ * used.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setDividerColor(@ColorInt int dividerColor) {
+ mDividerColor = dividerColor;
+ return this;
+ }
+
+ /**
+ * Sets whether it is allowed to expand a container to full screen by dragging the divider.
+ * Default is {@code true}.
+ */
+ @RequiresVendorApiLevel(level = 7)
+ @NonNull
+ public Builder setDraggingToFullscreenAllowed(boolean isDraggingToFullscreenAllowed) {
+ mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
+ return this;
+ }
+
+ /**
+ * Builds a {@link DividerAttributes} instance.
+ *
+ * @return a {@link DividerAttributes} instance.
+ * @throws IllegalArgumentException if the provided values are invalid.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public DividerAttributes build() {
+ return new DividerAttributes(mDividerType, mWidthDp, mPrimaryMinRatio,
+ mPrimaryMaxRatio, mDividerColor, mIsDraggingToFullscreenAllowed);
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
new file mode 100644
index 0000000..afea328
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.Objects;
+
+/**
+ * Describes the embedded window related info of an activity.
+ *
+ * @see ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback
+ * @see ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo
+ */
+public class EmbeddedActivityWindowInfo {
+
+ @NonNull
+ private final Activity mActivity;
+ private final boolean mIsEmbedded;
+ @NonNull
+ private final Rect mTaskBounds;
+ @NonNull
+ private final Rect mActivityStackBounds;
+
+ EmbeddedActivityWindowInfo(@NonNull Activity activity, boolean isEmbedded,
+ @NonNull Rect taskBounds, @NonNull Rect activityStackBounds) {
+ mActivity = Objects.requireNonNull(activity);
+ mIsEmbedded = isEmbedded;
+ mTaskBounds = Objects.requireNonNull(taskBounds);
+ mActivityStackBounds = Objects.requireNonNull(activityStackBounds);
+ }
+
+ /**
+ * Returns the {@link Activity} this {@link EmbeddedActivityWindowInfo} is about.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Whether this activity is embedded, which means it is in an ActivityStack window that
+ * doesn't fill the Task.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public boolean isEmbedded() {
+ return mIsEmbedded;
+ }
+
+ /**
+ * Returns the bounds of the Task window in display space.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Rect getTaskBounds() {
+ return mTaskBounds;
+ }
+
+ /**
+ * Returns the bounds of the ActivityStack window in display space.
+ * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when
+ * the activity is not embedded.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Rect getActivityStackBounds() {
+ return mActivityStackBounds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof EmbeddedActivityWindowInfo)) return false;
+ final EmbeddedActivityWindowInfo that = (EmbeddedActivityWindowInfo) o;
+ return mActivity.equals(that.mActivity)
+ && mIsEmbedded == that.mIsEmbedded
+ && mTaskBounds.equals(that.mTaskBounds)
+ && mActivityStackBounds.equals(that.mActivityStackBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mActivity.hashCode();
+ result = result * 31 + (mIsEmbedded ? 1 : 0);
+ result = result * 31 + mTaskBounds.hashCode();
+ result = result * 31 + mActivityStackBounds.hashCode();
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "EmbeddedActivityWindowInfo{"
+ + "activity=" + mActivity
+ + ", isEmbedded=" + mIsEmbedded
+ + ", taskBounds=" + mTaskBounds
+ + ", activityStackBounds=" + mActivityStackBounds
+ + "}";
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
new file mode 100644
index 0000000..e08b803
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 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.embedding;
+
+import android.content.res.Configuration;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+/**
+ * The parent container information of an {@link ActivityStack}.
+ * The data class is designed to provide information to calculate the presentation of
+ * an {@link ActivityStack}.
+ */
+@RequiresVendorApiLevel(level = 6)
+public class ParentContainerInfo {
+ @NonNull
+ private final WindowMetrics mWindowMetrics;
+
+ @NonNull
+ private final Configuration mConfiguration;
+
+ @NonNull
+ private final WindowLayoutInfo mWindowLayoutInfo;
+
+ /**
+ * {@link ParentContainerInfo} constructor, which is used in Window Manager Extensions to
+ * provide information of a parent window container.
+ *
+ * @param windowMetrics The parent container's {@link WindowMetrics}
+ * @param configuration The parent container's {@link Configuration}
+ * @param windowLayoutInfo The parent container's {@link WindowLayoutInfo}
+ */
+ ParentContainerInfo(@NonNull WindowMetrics windowMetrics, @NonNull Configuration configuration,
+ @NonNull WindowLayoutInfo windowLayoutInfo) {
+ mWindowMetrics = windowMetrics;
+ mConfiguration = configuration;
+ mWindowLayoutInfo = windowLayoutInfo;
+ }
+
+ /** Returns the parent container's {@link WindowMetrics}. */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public WindowMetrics getWindowMetrics() {
+ return mWindowMetrics;
+ }
+
+ /** Returns the parent container's {@link Configuration}. */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /** Returns the parent container's {@link WindowLayoutInfo}. */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public WindowLayoutInfo getWindowLayoutInfo() {
+ return mWindowLayoutInfo;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mWindowMetrics.hashCode();
+ result = 31 * result + mConfiguration.hashCode();
+ result = 31 * result + mWindowLayoutInfo.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof ParentContainerInfo)) return false;
+ final ParentContainerInfo parentContainerInfo = (ParentContainerInfo) obj;
+ return mWindowMetrics.equals(parentContainerInfo.mWindowMetrics)
+ && mConfiguration.equals(parentContainerInfo.mConfiguration)
+ && mWindowLayoutInfo.equals(parentContainerInfo.mWindowLayoutInfo);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return ParentContainerInfo.class.getSimpleName() + ": {"
+ + "windowMetrics=" + WindowMetricsCompat.toString(mWindowMetrics)
+ + ", configuration=" + mConfiguration
+ + ", windowLayoutInfo=" + mWindowLayoutInfo
+ + "}";
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
index 844443e..6d43da8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -65,7 +65,7 @@
*
* @see SplitAttributes.SplitType
* @see SplitAttributes.LayoutDirection
- * @see AnimationBackground
+ * @see AnimationParams
*/
@RequiresVendorApiLevel(level = 2)
public class SplitAttributes {
@@ -120,7 +120,7 @@
/**
* A window split that's based on the ratio of the size of the primary container to the
* size of the parent window (excluding area unavailable for the containers such as the
- * divider.
+ * divider. See {@link DividerAttributes}).
*
* <p>Values in the non-inclusive range (0.0, 1.0) define the size of
* the primary container relative to the size of the parent window:
@@ -144,7 +144,7 @@
*
* @param ratio The proportion of the parent window occupied by the primary container
* of the split (excluding area unavailable for the containers such as
- * the divider. Can be a value in the
+ * the divider. See {@link DividerAttributes}). Can be a value in the
* non-inclusive range (0.0, 1.0). Use
* {@link SplitType.ExpandContainersSplitType} to create a split
* type that occupies the entire parent window.
@@ -163,7 +163,7 @@
/**
* Gets the proportion of the parent window occupied by the primary activity
* container of the split (excluding area unavailable for the containers such as the
- * divider.
+ * divider. See {@link DividerAttributes}) .
*
* @return The proportion of the split occupied by the primary
* container.
@@ -378,11 +378,15 @@
private final SplitType mSplitType;
@NonNull
- private final AnimationBackground mAnimationBackground;
+ private final AnimationParams mAnimationParams;
@NonNull
private final WindowAttributes mWindowAttributes;
+ /** The attributes of a divider. If {@code null}, no divider is requested. */
+ @Nullable
+ private final DividerAttributes mDividerAttributes;
+
/**
* Creates an instance of this {@code SplitAttributes}.
*
@@ -391,22 +395,25 @@
* @param layoutDirection The layout direction of the split, such as left to
* right or top to bottom. See
* {@link SplitAttributes.LayoutDirection}.
- * @param animationBackground The {@link AnimationBackground} to use for the during animation
- * of the split involving this {@code SplitAttributes} object if the
- * animation requires a background.
+ * @param animationParams The {@link AnimationParams} to use for the during animation
+ * of the split involving this {@code SplitAttributes} object.
* @param attributes The {@link WindowAttributes} of the split, such as dim area
* behavior.
+ * @param dividerAttributes The {@link DividerAttributes}. If {@code null}, no divider is
+ * requested.
*/
SplitAttributes(
@NonNull SplitType splitType,
@ExtLayoutDirection int layoutDirection,
- @NonNull AnimationBackground animationBackground,
- @NonNull WindowAttributes attributes
+ @NonNull AnimationParams animationParams,
+ @NonNull WindowAttributes attributes,
+ @Nullable DividerAttributes dividerAttributes
) {
mSplitType = splitType;
mLayoutDirection = layoutDirection;
- mAnimationBackground = animationBackground;
+ mAnimationParams = animationParams;
mWindowAttributes = attributes;
+ mDividerAttributes = dividerAttributes;
}
/**
@@ -430,13 +437,25 @@
}
/**
- * Returns the {@link AnimationBackground} to use for the background during the
- * animation of the split involving this {@code SplitAttributes} object.
+ * @deprecated Use {@link #getAnimationParams()} starting with vendor API level 7. Only used if
+ * {@link #getAnimationParams()} can't be called on vendor API level 5 and 6.
*/
@NonNull
- @RequiresVendorApiLevel(level = 5)
+ @RequiresVendorApiLevel(level = 5, deprecatedSince = 7)
+ @Deprecated
+ @SuppressWarnings("Deprecation")
public AnimationBackground getAnimationBackground() {
- return mAnimationBackground;
+ return mAnimationParams.getAnimationBackground();
+ }
+
+ /**
+ * Returns the {@link AnimationParams} to use during the animation of the split involving
+ * this {@code SplitAttributes} object.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 7)
+ public AnimationParams getAnimationParams() {
+ return mAnimationParams;
}
/**
@@ -449,6 +468,13 @@
return mWindowAttributes;
}
+ /** Returns the {@link DividerAttributes}. If {@code null}, no divider is requested. */
+ @RequiresVendorApiLevel(level = 6)
+ @Nullable
+ public DividerAttributes getDividerAttributes() {
+ return mDividerAttributes;
+ }
+
/**
* Builder for creating an instance of {@link SplitAttributes}.
*
@@ -463,18 +489,34 @@
private int mLayoutDirection = LOCALE;
@NonNull
- private AnimationBackground mAnimationBackground =
- AnimationBackground.ANIMATION_BACKGROUND_DEFAULT;
+ private AnimationParams mAnimationParams = new AnimationParams.Builder().build();
@NonNull
private WindowAttributes mWindowAttributes =
new WindowAttributes(DIM_AREA_ON_TASK);
+ @Nullable
+ private DividerAttributes mDividerAttributes;
+
/** Creates a new {@link Builder} to create {@link SplitAttributes}. */
public Builder() {
}
/**
+ * Creates a {@link Builder} with values cloned from the original {@link SplitAttributes}.
+ *
+ * @param original the original {@link SplitAttributes} to initialize the {@link Builder}.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder(@NonNull SplitAttributes original) {
+ mSplitType = original.mSplitType;
+ mLayoutDirection = original.mLayoutDirection;
+ mAnimationParams = original.mAnimationParams;
+ mWindowAttributes = original.mWindowAttributes;
+ mDividerAttributes = original.mDividerAttributes;
+ }
+
+ /**
* Sets the split type attribute.
*
* The default is an equal split between primary and secondary
@@ -514,21 +556,31 @@
}
/**
- * Sets the {@link AnimationBackground} to use for the background during the
- * animation of the split involving this {@code SplitAttributes} object
- * if the animation requires a background.
+ * @deprecated Use {@link #setAnimationParams(AnimationParams)} starting with vendor API
+ * level 7. Only used if {@link #setAnimationParams(AnimationParams)} can't be called on
+ * vendor API level 5 and 6.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 5, deprecatedSince = 7)
+ @Deprecated
+ @SuppressWarnings("Deprecation")
+ public Builder setAnimationBackground(@NonNull AnimationBackground background) {
+ mAnimationParams =
+ new AnimationParams.Builder().setAnimationBackground(background).build();
+ return this;
+ }
+
+ /**
+ * Sets the {@link AnimationParams} to use during the animation of the split involving this
+ * {@code SplitAttributes} object.
*
- * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which
- * means to use the current theme window background color.
- *
- * @param background An {@link AnimationBackground} to be used for the animation of the
- * split.
+ * @param params The {@link AnimationParams} to be used for the animation of the split.
* @return This {@code Builder}.
*/
@NonNull
- @RequiresVendorApiLevel(level = 5)
- public Builder setAnimationBackground(@NonNull AnimationBackground background) {
- mAnimationBackground = background;
+ @RequiresVendorApiLevel(level = 7)
+ public Builder setAnimationParams(@NonNull AnimationParams params) {
+ mAnimationParams = params;
return this;
}
@@ -547,17 +599,25 @@
return this;
}
+ /** Sets the {@link DividerAttributes}. If {@code null}, no divider is requested. */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public Builder setDividerAttributes(@Nullable DividerAttributes dividerAttributes) {
+ mDividerAttributes = dividerAttributes;
+ return this;
+ }
+
/**
* Builds a {@link SplitAttributes} instance with the attributes
* specified by {@link #setSplitType}, {@link #setLayoutDirection}, and
- * {@link #setAnimationBackground}.
+ * {@link #setAnimationParams}.
*
* @return The new {@code SplitAttributes} instance.
*/
@NonNull
public SplitAttributes build() {
- return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackground,
- mWindowAttributes);
+ return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationParams,
+ mWindowAttributes, mDividerAttributes);
}
}
@@ -567,13 +627,15 @@
if (!(o instanceof SplitAttributes)) return false;
SplitAttributes that = (SplitAttributes) o;
return mLayoutDirection == that.mLayoutDirection && mSplitType.equals(that.mSplitType)
- && mAnimationBackground.equals(that.mAnimationBackground)
- && mWindowAttributes.equals(that.mWindowAttributes);
+ && Objects.equals(mAnimationParams, that.mAnimationParams)
+ && mWindowAttributes.equals(that.mWindowAttributes)
+ && Objects.equals(mDividerAttributes, that.mDividerAttributes);
}
@Override
public int hashCode() {
- return Objects.hash(mLayoutDirection, mSplitType, mAnimationBackground, mWindowAttributes);
+ return Objects.hash(mLayoutDirection, mSplitType, mAnimationParams, mWindowAttributes,
+ mDividerAttributes);
}
@NonNull
@@ -582,8 +644,9 @@
return SplitAttributes.class.getSimpleName() + "{"
+ "layoutDir=" + layoutDirectionToString()
+ ", splitType=" + mSplitType
- + ", animationBackground=" + mAnimationBackground
+ + ", animationParams=" + mAnimationParams
+ ", windowAttributes=" + mWindowAttributes
+ + ", dividerAttributes=" + mDividerAttributes
+ "}";
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
new file mode 100644
index 0000000..17bb15a
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 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.layout;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.util.SetUtilApi23;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a fold on a display that may intersect a window. The presence of a fold does not
+ * imply that it intersects the window an {@link android.app.Activity} is running in. For
+ * example, on a device that can fold like a book and has an outer screen, the fold should be
+ * reported regardless of the folding state, or which screen is on to indicate that there may
+ * be a fold when the user opens the device.
+ *
+ * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to listen to features
+ * that affect the window.
+ */
+public final class DisplayFoldFeature {
+
+ /**
+ * The type of fold is unknown. This is here for compatibility reasons if a new type is added,
+ * and cannot be reported to an incompatible application.
+ */
+ public static final int TYPE_UNKNOWN = 0;
+
+ /**
+ * The type of fold is a physical hinge separating two display panels.
+ */
+ public static final int TYPE_HINGE = 1;
+
+ /**
+ * The type of fold is a screen that folds from 0-180.
+ */
+ public static final int TYPE_SCREEN_FOLD_IN = 2;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {TYPE_UNKNOWN, TYPE_HINGE, TYPE_SCREEN_FOLD_IN})
+ public @interface FoldType {
+ }
+
+ /**
+ * The fold supports the half opened state.
+ */
+ public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1;
+
+ @Target(ElementType.TYPE_USE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {FOLD_PROPERTY_SUPPORTS_HALF_OPENED})
+ public @interface FoldProperty {
+ }
+
+ @FoldType
+ private final int mType;
+
+ private final Set<@FoldProperty Integer> mProperties;
+
+ /**
+ * Creates an instance of [FoldDisplayFeature].
+ *
+ * @param type the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or
+ * [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]
+ */
+ DisplayFoldFeature(@FoldType int type, @NonNull Set<@FoldProperty Integer> properties) {
+ mType = type;
+ if (Build.VERSION.SDK_INT >= 23) {
+ mProperties = SetUtilApi23.createSet();
+ } else {
+ mProperties = new HashSet<>();
+ }
+ mProperties.addAll(properties);
+ }
+
+ /**
+ * Returns the type of fold that is either a hinge or a fold.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @FoldType
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns {@code true} if the fold has the given property, {@code false} otherwise.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public boolean hasProperty(@FoldProperty int property) {
+ return mProperties.contains(property);
+ }
+ /**
+ * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public boolean hasProperties(@NonNull @FoldProperty int... properties) {
+ for (int i = 0; i < properties.length; i++) {
+ if (!mProperties.contains(properties[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DisplayFoldFeature that = (DisplayFoldFeature) o;
+ return mType == that.mType && Objects.equals(mProperties, that.mProperties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mProperties);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "ScreenFoldDisplayFeature{mType=" + mType + ", mProperties=" + mProperties + '}';
+ }
+
+ /**
+ * A builder to construct an instance of {@link DisplayFoldFeature}.
+ */
+ public static final class Builder {
+
+ @FoldType
+ private int mType;
+
+ private Set<@FoldProperty Integer> mProperties;
+
+ /**
+ * Constructs a builder to create an instance of {@link DisplayFoldFeature}.
+ *
+ * @param type the type of hinge for the {@link DisplayFoldFeature}.
+ * @see DisplayFoldFeature.FoldType
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder(@FoldType int type) {
+ mType = type;
+ if (Build.VERSION.SDK_INT >= 23) {
+ mProperties = SetUtilApi23.createSet();
+ } else {
+ mProperties = new HashSet<>();
+ }
+ }
+
+ /**
+ * Add a property to the set of properties exposed by {@link DisplayFoldFeature}.
+ */
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public Builder addProperty(@FoldProperty int property) {
+ mProperties.add(property);
+ return this;
+ }
+
+ /**
+ * Add a list of properties to the set of properties exposed by
+ * {@link DisplayFoldFeature}.
+ */
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public Builder addProperties(@NonNull @FoldProperty int... properties) {
+ for (int i = 0; i < properties.length; i++) {
+ mProperties.add(properties[i]);
+ }
+ return this;
+ }
+
+ /**
+ * Clear the properties in the builder.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public Builder clearProperties() {
+ mProperties.clear();
+ return this;
+ }
+
+ /**
+ * Returns an instance of {@link DisplayFoldFeature}.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ public DisplayFoldFeature build() {
+ return new DisplayFoldFeature(mType, mProperties);
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
new file mode 100644
index 0000000..2143e29
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 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.layout;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to represent all the possible features that may interact with or appear in a window,
+ * that an application might want to respond to.
+ */
+public final class SupportedWindowFeatures {
+
+ private final List<DisplayFoldFeature> mDisplayFoldFeatureList;
+
+ private SupportedWindowFeatures(
+ @NonNull List<DisplayFoldFeature> displayFoldFeatureList) {
+ mDisplayFoldFeatureList = new ArrayList<>(displayFoldFeatureList);
+ }
+
+ /**
+ * Returns the possible {@link DisplayFoldFeature}s that can be reported to an application.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public List<DisplayFoldFeature> getDisplayFoldFeatures() {
+ return new ArrayList<>(mDisplayFoldFeatureList);
+ }
+
+
+ /**
+ * A class to create a {@link SupportedWindowFeatures} instance.
+ */
+ public static final class Builder {
+
+ private final List<DisplayFoldFeature> mDisplayFoldFeatures;
+
+ /**
+ * Creates a new instance of {@link Builder}
+ */
+ @RequiresVendorApiLevel(level = 6)
+ public Builder(@NonNull List<DisplayFoldFeature> displayFoldFeatures) {
+ mDisplayFoldFeatures = new ArrayList<>(displayFoldFeatures);
+ }
+
+ /**
+ * Creates an instance of {@link SupportedWindowFeatures} with the features set.
+ */
+ @NonNull
+ @RequiresVendorApiLevel(level = 6)
+ public SupportedWindowFeatures build() {
+ return new SupportedWindowFeatures(mDisplayFoldFeatures);
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
index 875f763..d55e341 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
@@ -96,4 +96,17 @@
throw new UnsupportedOperationException("This method must not be called unless there is a"
+ " corresponding override implementation on the device.");
}
+
+ /**
+ * Returns the {@link SupportedWindowFeatures} for the device. This value will not change
+ * over time.
+ * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to register a
+ * listener for features that impact the window.
+ */
+ @RequiresVendorApiLevel(level = 6)
+ @NonNull
+ default SupportedWindowFeatures getSupportedWindowFeatures() {
+ throw new UnsupportedOperationException("This method will not be called unless there is a"
+ + " corresponding override implementation on the device");
+ }
}
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
new file mode 100644
index 0000000..69d0784
--- /dev/null
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 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.embedding;
+
+import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_DRAGGABLE;
+import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_FIXED;
+import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT;
+import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Verifies {@link DividerAttributes} behavior. */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class DividerAttributesTest {
+
+ @Test
+ public void testDividerAttributesDefaults() {
+ final DividerAttributes defaultAttrs =
+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED).build();
+ assertThat(defaultAttrs.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
+ assertThat(defaultAttrs.getWidthDp()).isEqualTo(WIDTH_SYSTEM_DEFAULT);
+ assertThat(defaultAttrs.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+ assertThat(defaultAttrs.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+ }
+
+ @Test
+ public void testDividerAttributesBuilder() {
+ final DividerAttributes dividerAttributes1 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+ .setWidthDp(20)
+ .build();
+ assertThat(dividerAttributes1.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
+ assertThat(dividerAttributes1.getWidthDp()).isEqualTo(20);
+ assertThat(dividerAttributes1.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+ assertThat(dividerAttributes1.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+ final DividerAttributes dividerAttributes2 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.2f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+ assertThat(dividerAttributes2.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+ assertThat(dividerAttributes2.getWidthDp()).isEqualTo(20);
+ assertThat(dividerAttributes2.getPrimaryMinRatio()).isEqualTo(0.2f);
+ assertThat(dividerAttributes2.getPrimaryMaxRatio()).isEqualTo(0.8f);
+
+ final DividerAttributes dividerAttributes3 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .build();
+ assertThat(dividerAttributes3.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+ assertThat(dividerAttributes3.getWidthDp()).isEqualTo(20);
+ assertThat(dividerAttributes3.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+ assertThat(dividerAttributes3.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+ final DividerAttributes dividerAttributes4 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.2f)
+ .build();
+ assertThat(dividerAttributes4.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+ assertThat(dividerAttributes4.getWidthDp()).isEqualTo(20);
+ assertThat(dividerAttributes4.getPrimaryMinRatio()).isEqualTo(0.2f);
+ assertThat(dividerAttributes4.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+ final DividerAttributes dividerAttributes5 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMaxRatio(0.2f)
+ .build();
+ assertThat(dividerAttributes5.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+ assertThat(dividerAttributes5.getWidthDp()).isEqualTo(20);
+ assertThat(dividerAttributes5.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+ assertThat(dividerAttributes5.getPrimaryMaxRatio()).isEqualTo(0.2f);
+ }
+
+ @Test
+ public void testDividerAttributesEquals() {
+ final DividerAttributes dividerAttributes1 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.2f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ final DividerAttributes dividerAttributes2 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.2f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ final DividerAttributes dividerAttributes3 =
+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+ .setWidthDp(20)
+ .build();
+
+ assertThat(dividerAttributes1).isEqualTo(dividerAttributes2);
+ assertThat(dividerAttributes1).isNotEqualTo(dividerAttributes3);
+ }
+
+ @Test
+ public void testDividerAttributesValidation() {
+ assertThrows(
+ "Must not set min max ratio for DIVIDER_TYPE_FIXED",
+ IllegalStateException.class,
+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+ .setPrimaryMinRatio(0.2f)
+ .setPrimaryMaxRatio(0.8f)
+ .build()
+ );
+
+ assertThrows(
+ "Min ratio must be less than or equal to max ratio",
+ IllegalStateException.class,
+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.8f)
+ .setPrimaryMaxRatio(0.2f)
+ .build()
+ );
+
+ assertThrows(
+ "Min ratio must be in range [0.0, 1.0] or RATIO_UNSET",
+ IllegalArgumentException.class,
+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(2.0f)
+ );
+
+ assertThrows(
+ "Max ratio must be in range [0.0, 1.0] or RATIO_UNSET",
+ IllegalArgumentException.class,
+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMaxRatio(2.0f)
+ );
+
+ assertThrows(
+ "Width must be greater than or equal to zero or WIDTH_UNSET",
+ IllegalArgumentException.class,
+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(-10)
+ );
+ }
+}
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
index 1c998a0..7bffb2b 100644
--- a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
@@ -36,30 +36,51 @@
public class SplitAttributesTest {
@Test
public void testSplitAttributesEquals() {
+ final AnimationParams animParams1 =
+ new AnimationParams.Builder()
+ .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+ .build();
final SplitAttributes attrs1 = new SplitAttributes.Builder()
.setSplitType(splitEqually())
.setLayoutDirection(LayoutDirection.LOCALE)
- .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setAnimationParams(animParams1)
.build();
+ final AnimationParams animParams2 =
+ new AnimationParams.Builder()
+ .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+ .build();
final SplitAttributes attrs2 = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
.setLayoutDirection(LayoutDirection.LOCALE)
- .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setAnimationParams(animParams2)
.build();
+ final AnimationParams animParams3 =
+ new AnimationParams.Builder()
+ .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+ .build();
final SplitAttributes attrs3 = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
.setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
- .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+ .setAnimationParams(animParams3)
.build();
+ final AnimationParams animParams4and5 =
+ new AnimationParams.Builder()
+ .setAnimationBackground(
+ AnimationBackground.createColorBackground(Color.BLUE))
+ .setOpenAnimationResId(android.R.anim.fade_in)
+ .build();
final SplitAttributes attrs4 = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
.setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
- .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
+ .setAnimationParams(animParams4and5)
.build();
final SplitAttributes attrs5 = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
.setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
- .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
+ .setAnimationParams(animParams4and5)
.build();
assertNotEquals(attrs1, attrs2);
@@ -79,6 +100,18 @@
}
@Test
+ public void testSplitAttributesEqualsUsingBuilderFromExistingInstance() {
+ final AnimationParams animParamsDefault = new AnimationParams.Builder().build();
+ final SplitAttributes attrs1 = new SplitAttributes.Builder()
+ .setSplitType(splitEqually())
+ .setLayoutDirection(LayoutDirection.LOCALE)
+ .setAnimationParams(animParamsDefault)
+ .build();
+ final SplitAttributes attrs2 = new SplitAttributes.Builder(attrs1).build();
+ assertEquals(attrs1, attrs2);
+ }
+
+ @Test
public void testSplitTypeEquals() {
final SplitAttributes.SplitType[] splitTypes = new SplitAttributes.SplitType[]{
new SplitAttributes.SplitType.ExpandContainersSplitType(),
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
new file mode 100644
index 0000000..e9937a0
--- /dev/null
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.layout;
+
+import static androidx.window.extensions.layout.DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED;
+import static androidx.window.extensions.layout.DisplayFoldFeature.TYPE_HINGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DisplayFoldFeatureTest {
+
+ @Test
+ public void test_builder_matches_constructor() {
+ Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
+ properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
+ DisplayFoldFeature expected = new DisplayFoldFeature(TYPE_HINGE, properties);
+
+ DisplayFoldFeature actual = new DisplayFoldFeature.Builder(TYPE_HINGE)
+ .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+ .build();
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void test_equals_matches() {
+ Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
+ properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
+ DisplayFoldFeature first = new DisplayFoldFeature(TYPE_HINGE, properties);
+ DisplayFoldFeature second = new DisplayFoldFeature(TYPE_HINGE, properties);
+
+ assertEquals(first, second);
+ assertEquals(first.hashCode(), second.hashCode());
+ }
+
+ @Test
+ public void test_getter_matches_values() {
+ final int type = TYPE_HINGE;
+ DisplayFoldFeature actual = new DisplayFoldFeature.Builder(type)
+ .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+ .build();
+
+ assertEquals(type, actual.getType());
+ assertTrue(actual.hasProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
+ assertTrue(actual.hasProperties(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
+ }
+}
diff --git a/window/integration-tests/macrobenchmark-target/build.gradle b/window/integration-tests/macrobenchmark-target/build.gradle
index 8bec782..1631c90 100644
--- a/window/integration-tests/macrobenchmark-target/build.gradle
+++ b/window/integration-tests/macrobenchmark-target/build.gradle
@@ -21,6 +21,7 @@
}
android {
+ compileSdkVersion 35
defaultConfig {
minSdk 28
}
diff --git a/window/window-demos/demo-common/build.gradle b/window/window-demos/demo-common/build.gradle
index a828dd0..27c4c9c 100644
--- a/window/window-demos/demo-common/build.gradle
+++ b/window/window-demos/demo-common/build.gradle
@@ -16,13 +16,16 @@
plugins {
id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
id("com.android.library")
id("kotlin-android")
}
android {
+ compileSdk 35
defaultConfig {
minSdkVersion 23
+ targetSdkVersion 35
}
buildFeatures {
viewBinding true
@@ -31,9 +34,9 @@
}
dependencies {
- implementation("androidx.appcompat:appcompat:1.2.0")
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.core:core-ktx:1.3.2")
- implementation("androidx.activity:activity:1.2.0")
+ implementation("androidx.activity:activity:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.2.1")
api(libs.constraintLayout)
// TODO(b/152245564) Conflicting dependencies cause IDE errors.
@@ -42,6 +45,11 @@
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.startup:startup-runtime:1.1.0")
+ // Compose dependencies
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation("androidx.compose.material3:material3:1.2.1")
+
implementation(project(":window:window-java"))
debugImplementation(libs.leakcanary)
}
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt
new file mode 100644
index 0000000..686c802
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://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.demo.common
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.colorResource
+
+@Composable
+fun DemoTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ if (darkTheme) {
+ darkColorScheme(
+ primary = colorResource(R.color.colorPrimaryDark),
+ secondary = colorResource(R.color.colorAccent),
+ )
+ } else {
+ lightColorScheme(
+ primary = colorResource(R.color.colorPrimary),
+ secondary = colorResource(R.color.colorAccent),
+ )
+ }
+
+ MaterialTheme(colorScheme = colorScheme, content = content)
+}
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
index fee0c73..bef6cf6 100644
--- a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
@@ -21,7 +21,6 @@
import android.view.Menu
import android.view.MenuItem
import android.view.View
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -40,7 +39,7 @@
import kotlinx.coroutines.launch
/** Demo activity that shows all display features and current device state on the screen. */
-open class DisplayFeaturesActivity : AppCompatActivity() {
+open class DisplayFeaturesActivity : EdgeToEdgeActivity() {
private val infoLogAdapter = InfoLogAdapter()
private val displayFeatureViews = ArrayList<View>()
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt
new file mode 100644
index 0000000..06bebf78
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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.demo.common
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+
+/** An activity to make its UI content edge-to-edge and fits system windows. */
+open class EdgeToEdgeActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun setContentView(view: View?) {
+ super.setContentView(view)
+ view?.fitsSystemWindows = true
+ }
+}
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
index 454d6fe..5963f3df 100644
--- a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
@@ -53,6 +53,11 @@
++id
}
+ fun appendAndNotify(title: String, message: String) {
+ append(title, message)
+ notifyDataSetChanged()
+ }
+
private fun append(item: InfoLog) {
items.add(0, item)
}
diff --git a/window/window-demos/demo-second-app/build.gradle b/window/window-demos/demo-second-app/build.gradle
index 336950f..c39a226 100644
--- a/window/window-demos/demo-second-app/build.gradle
+++ b/window/window-demos/demo-second-app/build.gradle
@@ -21,9 +21,11 @@
}
android {
+ compileSdk 35
defaultConfig {
applicationId "androidx.window.demo2"
minSdkVersion 23
+ targetSdkVersion 35
}
buildFeatures {
viewBinding true
@@ -32,8 +34,8 @@
}
dependencies {
- implementation("androidx.activity:activity:1.2.0")
- implementation("androidx.appcompat:appcompat:1.2.0")
+ implementation("androidx.activity:activity:1.9.0")
+ implementation(project(":appcompat:appcompat"))
api(libs.constraintLayout)
implementation("androidx.core:core-ktx:1.8.0")
// TODO(b/152245564) Conflicting dependencies cause IDE errors.
diff --git a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
index 15d5101..177762b 100644
--- a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
android:supportsRtl="true">
<activity
android:name=".embedding.TrustedEmbeddingActivity"
+ android:supportsPictureInPicture="true"
android:exported="true"
android:label="@string/trusted_embedding_activity"
android:configChanges=
@@ -30,6 +31,7 @@
</activity>
<activity
android:name=".embedding.UntrustedEmbeddingActivity"
+ android:supportsPictureInPicture="true"
android:exported="true"
android:label="@string/untrusted_embedding_activity"
android:configChanges=
@@ -39,6 +41,11 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <!-- Require to restore the split after entering and exiting picture-in-picture mode
+ for untrusted embedding. -->
+ <property
+ android:name="android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING"
+ android:value="true" />
</activity>
<activity-alias
android:name="androidx.window.demo2.DisplayFeaturesActivity"
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
new file mode 100644
index 0000000..e3406ce
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 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.demo2.embedding
+
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.os.Bundle
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.common.util.PictureInPictureUtil
+import androidx.window.demo2.R
+import androidx.window.demo2.databinding.ActivityEmbeddedBinding
+import androidx.window.embedding.ActivityEmbeddingController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+open class EmbeddedActivityBase : EdgeToEdgeActivity() {
+ lateinit var viewBinding: ActivityEmbeddedBinding
+ private lateinit var activityEmbeddingController: ActivityEmbeddingController
+ private lateinit var windowInfoView: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewBinding = ActivityEmbeddedBinding.inflate(layoutInflater)
+ setContentView(viewBinding.root)
+ viewBinding.buttonPip.setOnClickListener {
+ PictureInPictureUtil.startPictureInPicture(this, false)
+ }
+ viewBinding.buttonStartActivity.setOnClickListener {
+ startActivity(Intent(this, this.javaClass))
+ }
+ viewBinding.buttonStartActivityFromApplicationContext.setOnClickListener {
+ application.startActivity(Intent(this, this.javaClass).setFlags(FLAG_ACTIVITY_NEW_TASK))
+ }
+
+ activityEmbeddingController = ActivityEmbeddingController.getInstance(this)
+ initializeEmbeddedActivityInfoCallback()
+ }
+
+ private fun initializeEmbeddedActivityInfoCallback() {
+ val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
+ if (extensionVersion < 6) {
+ // EmbeddedActivityWindowInfo is only available on 6+.
+ return
+ }
+
+ windowInfoView = viewBinding.windowIntoText
+ lifecycleScope.launch(Dispatchers.Main) {
+ // Collect EmbeddedActivityWindowInfo when STARTED and stop when STOPPED.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // After register, the flow will be triggered immediately if the activity is
+ // embedded.
+ // However, if the activity is changed to non-embedded state in background (after
+ // STOPPED), the flow will not report the change (because it has been unregistered).
+ // Reset before start listening.
+ resetWindowInfoView()
+ activityEmbeddingController
+ .embeddedActivityWindowInfo(this@EmbeddedActivityBase)
+ .collect { info ->
+ if (info.isEmbedded) {
+ windowInfoView.text = info.toString()
+ } else {
+ resetWindowInfoView()
+ }
+ }
+ }
+ }
+ }
+
+ private fun resetWindowInfoView() {
+ windowInfoView.text = getString(R.string.embedded_window_info_unavailable)
+ }
+}
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
index 67c0b09..1a2ea23 100644
--- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
@@ -16,9 +16,7 @@
package androidx.window.demo2.embedding
-import android.app.Activity
import android.os.Bundle
-import android.widget.TextView
import androidx.window.demo2.R
/**
@@ -26,12 +24,10 @@
* `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from the
* split demos in window-samples/demos.
*/
-class TrustedEmbeddingActivity : Activity() {
+class TrustedEmbeddingActivity : EmbeddedActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_embedded)
- findViewById<TextView>(R.id.detail_text_view).text =
- getString(R.string.trusted_embedding_activity_detail)
+ viewBinding.detailTextView.text = getString(R.string.trusted_embedding_activity_detail)
}
}
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
index b8405f4..487e4fa 100644
--- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
@@ -16,21 +16,17 @@
package androidx.window.demo2.embedding
-import android.app.Activity
import android.os.Bundle
-import android.widget.TextView
import androidx.window.demo2.R
/**
* Activity that can be embedded in untrusted mode. See `android:allowUntrustedActivityEmbedding` in
* AndroidManifest. Activity can be launched from the split demos in window-samples/demos.
*/
-class UntrustedEmbeddingActivity : Activity() {
+class UntrustedEmbeddingActivity : EmbeddedActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_embedded)
- findViewById<TextView>(R.id.detail_text_view).text =
- getString(R.string.untrusted_embedding_activity_detail)
+ viewBinding.detailTextView.text = getString(R.string.untrusted_embedding_activity_detail)
}
}
diff --git a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
index fbd572c..102145e 100644
--- a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
+++ b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
@@ -15,9 +15,13 @@
limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="10dp"
+ android:background="@color/colorAccent">
<TextView
android:layout_gravity="center"
@@ -25,4 +29,44 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
-</FrameLayout>
\ No newline at end of file
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA" />
+
+ <TextView
+ android:layout_gravity="center"
+ android:id="@+id/window_into_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/embedded_window_info_unavailable"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA" />
+
+ <Button
+ android:id="@+id/button_pip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="ENTER PIP" />
+ <Button
+ android:id="@+id/button_start_activity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="Start a new instance of current Activity" />
+ <Button
+ android:id="@+id/button_start_activity_from_application_context"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="Start a new instance of current Activity from Application Context" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo-second-app/src/main/res/values/strings.xml b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
index de4172b..7fa3d6d 100644
--- a/window/window-demos/demo-second-app/src/main/res/values/strings.xml
+++ b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
@@ -22,4 +22,6 @@
<string name="untrusted_embedding_activity">Untrusted Embedding Activity</string>
<string name="untrusted_embedding_activity_detail">Activity allows embedding in untrusted mode
via opt-in.</string>
+ <string name="embedded_window_info_unavailable">EmbeddedActivityWindowInfo not available
+ </string>
</resources>
\ No newline at end of file
diff --git a/window/window-demos/demo/build.gradle b/window/window-demos/demo/build.gradle
index 615fea8..fc2d898 100644
--- a/window/window-demos/demo/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -21,19 +21,22 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.LibraryType
+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
+ compileSdk 35
defaultConfig {
applicationId "androidx.window.demo"
minSdkVersion 23
+ targetSdkVersion 35
}
buildFeatures {
viewBinding true
@@ -65,9 +68,9 @@
}
dependencies {
- implementation("androidx.appcompat:appcompat:1.5.1")
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.core:core-ktx:1.3.2")
- implementation("androidx.activity:activity:1.2.0")
+ implementation("androidx.activity:activity:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.2.1")
// TODO(b/262583150): force tracing 1.1.0 since its required by androidTest
implementation("androidx.tracing:tracing:1.1.0")
@@ -77,8 +80,20 @@
implementation "androidx.browser:browser:1.3.0"
implementation("androidx.startup:startup-runtime:1.1.0")
+ // Compose dependencies
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:ui:ui-tooling-preview"))
+ debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation("androidx.activity:activity-compose:1.9.0")
+ implementation("androidx.compose.material3:material3:1.2.1")
+ implementation("androidx.compose.material:material-icons-extended:1.6.8")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
+
implementation(project(":window:window-java"))
implementation(project(":window:window-demos:demo-common"))
+
debugImplementation(libs.leakcanary)
androidTestImplementation(libs.testCore)
@@ -89,7 +104,7 @@
androidTestImplementation(project(":window:window-testing"))
androidTestImplementation(project(":window:window-demos:demo-common"))
- coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
+ coreLibraryDesugaring(libs.desugarJdkLibs)
}
// Allow usage of Kotlin's @OptIn.
diff --git a/window/window-demos/demo/src/main/AndroidManifest.xml b/window/window-demos/demo/src/main/AndroidManifest.xml
index 5b81535..214ca2d 100644
--- a/window/window-demos/demo/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo/src/main/AndroidManifest.xml
@@ -22,18 +22,12 @@
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
android:value="true" />
+ <!-- The app itself supports activity embedding, so a system override is not needed. -->
+ <property
+ android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
+ android:value="false" />
- <service android:name=".TestIme"
- android:label="@string/test_ime"
- android:permission="android.permission.BIND_INPUT_METHOD"
- android:exported="true">
- <intent-filter>
- <action android:name="android.view.InputMethod"/>
- </intent-filter>
- <meta-data android:name="android.view.im"
- android:resource="@xml/method"/>
- </service>
-
+ <!--region WindowManager Demos -->
<activity android:name=".demos.WindowDemosActivity"
android:exported="true"
android:label="@string/windowManagerDemos">
@@ -74,6 +68,9 @@
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|screenSize"
android:label="@string/window_metrics"/>
+ <!--endregion WindowManager Demos -->
+
+ <!--region Rear Display Demos -->
<activity android:name=".area.RearDisplayActivityConfigChanges"
android:exported="true"
android:configChanges=
@@ -84,6 +81,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity android:name=".area.RearDisplayPresentationActivity"
+ android:exported="false"
+ android:configChanges="orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+ android:label="@string/dual_display" />
+ <!--endregion Rear Display Demos -->
+
+ <!--region Sample showcase of split activity rules -->
<activity
android:name=".embedding.SplitActivityA"
android:exported="true"
@@ -132,10 +136,17 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
<activity
+ android:name=".embedding.DialogActivity"
+ android:theme="@style/Theme.AppCompat.Dialog"
+ android:exported="false"
+ android:label="Dialog Activity"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+ android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
+ <activity
android:name=".embedding.ExpandedDialogActivity"
android:theme="@style/ExpandedDialogTheme"
android:exported="false"
- android:label="Dialog Activity"
+ android:label="Expanded Dialog Activity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
<activity
@@ -176,9 +187,9 @@
android:label="Placeholder"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.list_detail_split_affinity" />
+ <!--endregion Sample showcase of split activity rules -->
- <!-- Split PiP App -->
-
+ <!-- region Split PiP App -->
<activity
android:name=".embedding.SplitPipActivityA"
android:exported="true"
@@ -213,10 +224,11 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.split_pip">
</activity>
+ <!-- endregion Split PiP App -->
+ <!-- region Split on Device State -->
<!-- The demo App to show how to change the current split layout with the current device and
window states -->
-
<activity
android:name=".embedding.SplitDeviceStateActivityA"
android:exported="true"
@@ -235,7 +247,9 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.split_device_state_activity_affinity">
</activity>
+ <!-- endregion Split on Device State -->
+ <!-- region Split Toggle at Runtime -->
<!-- The demo app to show how to change layout with runtime APIs -->
<activity
android:name=".embedding.SplitAttributesToggleMainActivity"
@@ -263,16 +277,26 @@
android:label="Split Toggle Placeholder"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.split_attributes_toggle_activity_affinity" />
+ <!-- endregion Split Toggle at Runtime -->
+ <!-- region IME usages in ActivityEmbedding split -->
<!-- The demo app that shows various IME-related use cases -->
-
<activity android:name=".ImeActivity"
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|screenSize"
- android:label="@string/ime"/>
-
+ android:label="@string/ime" />
+ <service android:name=".TestIme"
+ android:label="@string/test_ime"
+ android:permission="android.permission.BIND_INPUT_METHOD"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.view.InputMethod" />
+ </intent-filter>
+ <meta-data
+ android:name="android.view.im"
+ android:resource="@xml/method" />
+ </service>
<!-- The demo app to show IME usages in ActivityEmbedding split. -->
-
<activity
android:name=".embedding.SplitImeActivityMain"
android:exported="true"
@@ -291,9 +315,37 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
android:taskAffinity="androidx.window.demo.split_ime">
</activity>
+ <!-- endregion IME usages in ActivityEmbedding split -->
+ <!-- region Overlay features -->
+ <!-- The demo app to show how to use overlay features -->
+ <activity
+ android:name=".embedding.OverlayAssociatedActivityA"
+ android:exported="true"
+ android:label="Overlay Activity A"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+ android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".embedding.OverlayAssociatedActivityB"
+ android:exported="true"
+ android:label="Overlay Activity B"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+ android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
+ </activity>
+ <activity-alias
+ android:name=".embedding.SplitWithOverlayActivity"
+ android:targetActivity=".embedding.SplitActivityDetail"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+ android:taskAffinity="androidx.window.demo.overlay_activity_affinity" />
+ <!-- endregion Overlay features -->
+
+ <!-- region Display configuration callbacks -->
<!-- The demo app to show display configuration from different system callbacks. -->
-
<activity
android:name=".coresdk.WindowStateCallbackActivity"
android:exported="true"
@@ -305,9 +357,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <!-- endregion Display configuration callbacks -->
- <!-- Activity embedding initializer -->
-
+ <!-- region Activity embedding initializer -->
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
@@ -316,11 +368,7 @@
<meta-data android:name="androidx.window.demo.embedding.ExampleWindowInitializer"
android:value="androidx.startup" />
</provider>
-
- <!-- The app itself supports activity embedding, so a system override is not needed. -->
- <property
- android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
- android:value="false" />
+ <!-- endregion Activity embedding initializer -->
</application>
</manifest>
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
index 898ce04..f639f70 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
@@ -21,10 +21,10 @@
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import android.widget.Button
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
/** Demo app that shows various IME-related features. */
-class ImeActivity : AppCompatActivity() {
+class ImeActivity : EdgeToEdgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
index 51b4af7..1699741 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
@@ -27,10 +27,10 @@
import android.view.View
import android.widget.TextView
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
@@ -41,7 +41,7 @@
* Demo activity that reacts to foldable device state change and shows content on the outside
* display when the device is folded.
*/
-class PresentationActivity : AppCompatActivity() {
+class PresentationActivity : EdgeToEdgeActivity() {
private val TAG = "FoldablePresentation"
private var presentation: DemoPresentation? = null
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
index e3b89a2..cf29e6d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
@@ -17,16 +17,16 @@
package androidx.window.demo
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.layout.WindowInfoTracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/** Demo of [SplitLayout]. */
-class SplitLayoutActivity : AppCompatActivity() {
+class SplitLayoutActivity : EdgeToEdgeActivity() {
private lateinit var splitLayout: SplitLayout
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
index 2a13682..06c94dc 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
@@ -78,7 +78,8 @@
.append(
"Width: $width, Height: $height\n" +
"Top: ${windowMetrics.bounds.top}, Bottom: ${windowMetrics.bounds.bottom}, " +
- "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}"
+ "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}\n" +
+ "Density: ${windowMetrics.density}"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
index 4bdc573..8afa7d5 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
@@ -18,12 +18,12 @@
import android.content.res.Configuration
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.common.infolog.InfoLogAdapter
import androidx.window.layout.WindowMetricsCalculator
-class WindowMetricsActivity : AppCompatActivity() {
+class WindowMetricsActivity : EdgeToEdgeActivity() {
private val adapter = InfoLogAdapter()
@@ -45,7 +45,10 @@
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val width = windowMetrics.bounds.width()
val height = windowMetrics.bounds.height()
- adapter.append("WindowMetrics update", "width: $width, height: $height")
+ adapter.append(
+ "WindowMetrics update",
+ "width: $width, height: $height, " + "density: ${windowMetrics.density}"
+ )
adapter.notifyDataSetChanged()
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
index bff1424..d0514a1 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
@@ -17,23 +17,20 @@
package androidx.window.demo.area
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.area.WindowAreaCapability
import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_TRANSFER_ACTIVITY_TO_AREA
-import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNAVAILABLE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
import androidx.window.area.WindowAreaController
import androidx.window.area.WindowAreaInfo
-import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
import androidx.window.area.WindowAreaSession
import androidx.window.area.WindowAreaSessionCallback
import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.common.infolog.InfoLogAdapter
import androidx.window.demo.databinding.ActivityRearDisplayBinding
import java.text.SimpleDateFormat
@@ -41,9 +38,6 @@
import java.util.Locale
import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
/**
@@ -53,15 +47,14 @@
* This Activity overrides configuration changes for simplicity.
*/
@OptIn(ExperimentalWindowApi::class)
-class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
+class RearDisplayActivityConfigChanges : EdgeToEdgeActivity(), WindowAreaSessionCallback {
private lateinit var windowAreaController: WindowAreaController
private var rearDisplaySession: WindowAreaSession? = null
- private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
- private var rearDisplayStatus: WindowAreaCapability.Status = WINDOW_AREA_STATUS_UNSUPPORTED
private val infoLogAdapter = InfoLogAdapter()
private lateinit var binding: ActivityRearDisplayBinding
private lateinit var executor: Executor
+ private var currentWindowAreaInfo: WindowAreaInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -73,59 +66,77 @@
binding.rearStatusRecyclerView.adapter = infoLogAdapter
- binding.rearDisplayButton.setOnClickListener {
- if (rearDisplayStatus == WINDOW_AREA_STATUS_ACTIVE) {
- if (rearDisplaySession == null) {
- rearDisplaySession =
- rearDisplayWindowAreaInfo?.getActiveSession(
- OPERATION_TRANSFER_ACTIVITY_TO_AREA
- )
+ lifecycleScope.launch(Dispatchers.Main) {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Safely collect from windowInfoRepo when the lifecycle is STARTED
+ // and stops collection when the lifecycle is STOPPED
+ windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "number of areas: " + windowAreaInfos.size
+ )
+ windowAreaInfos.forEach { windowAreaInfo ->
+ if (windowAreaInfo.type == WindowAreaInfo.Type.TYPE_REAR_FACING) {
+ currentWindowAreaInfo = windowAreaInfo
+ val transferCapability =
+ windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)
+ infoLogAdapter.append(
+ getCurrentTimeString(),
+ transferCapability.status.toString() +
+ " : " +
+ windowAreaInfo.metrics.toString()
+ )
+ updateRearDisplayButton()
+ }
+ }
+ infoLogAdapter.notifyDataSetChanged()
}
+ }
+ }
+
+ binding.rearDisplayButton.setOnClickListener {
+ if (rearDisplaySession != null) {
rearDisplaySession?.close()
} else {
- rearDisplayWindowAreaInfo?.token?.let { token ->
+ currentWindowAreaInfo?.let {
windowAreaController.transferActivityToWindowArea(
- token = token,
- activity = this,
- executor = executor,
- windowAreaSessionCallback = this
+ it.token,
+ this,
+ executor,
+ this
)
}
}
}
- lifecycleScope.launch(Dispatchers.Main) {
- lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- windowAreaController.windowAreaInfos
- .map { windowAreaInfoList ->
- windowAreaInfoList.firstOrNull { windowAreaInfo ->
- windowAreaInfo.type == TYPE_REAR_FACING
- }
- }
- .onEach { windowAreaInfo -> rearDisplayWindowAreaInfo = windowAreaInfo }
- .map(this@RearDisplayActivityConfigChanges::getRearDisplayStatus)
- .distinctUntilChanged()
- .collect { status ->
- infoLogAdapter.append(getCurrentTimeString(), status.toString())
- infoLogAdapter.notifyDataSetChanged()
- rearDisplayStatus = status
- updateRearDisplayButton()
- }
+ binding.rearDisplaySessionButton.setOnClickListener {
+ if (rearDisplaySession == null) {
+ try {
+ rearDisplaySession =
+ currentWindowAreaInfo?.getActiveSession(OPERATION_TRANSFER_ACTIVITY_TO_AREA)
+ updateRearDisplayButton()
+ } catch (e: IllegalStateException) {
+ infoLogAdapter.appendAndNotify(getCurrentTimeString(), e.toString())
+ }
}
}
}
override fun onSessionStarted(session: WindowAreaSession) {
rearDisplaySession = session
- infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
- infoLogAdapter.notifyDataSetChanged()
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "RearDisplay Session has been started"
+ )
updateRearDisplayButton()
}
override fun onSessionEnded(t: Throwable?) {
rearDisplaySession = null
- infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
- infoLogAdapter.notifyDataSetChanged()
+ infoLogAdapter.appendAndNotify(getCurrentTimeString(), "RearDisplay Session has ended")
updateRearDisplayButton()
}
@@ -135,22 +146,24 @@
binding.rearDisplayButton.text = "Disable RearDisplay Mode"
return
}
- when (rearDisplayStatus) {
- WINDOW_AREA_STATUS_UNSUPPORTED -> {
- binding.rearDisplayButton.isEnabled = false
- binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
- }
- WINDOW_AREA_STATUS_UNAVAILABLE -> {
- binding.rearDisplayButton.isEnabled = false
- binding.rearDisplayButton.text = "RearDisplay is not currently available"
- }
- WINDOW_AREA_STATUS_AVAILABLE -> {
- binding.rearDisplayButton.isEnabled = true
- binding.rearDisplayButton.text = "Enable RearDisplay Mode"
- }
- WINDOW_AREA_STATUS_ACTIVE -> {
- binding.rearDisplayButton.isEnabled = true
- binding.rearDisplayButton.text = "Disable RearDisplay Mode"
+ currentWindowAreaInfo?.let { windowAreaInfo ->
+ when (windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA).status) {
+ WINDOW_AREA_STATUS_UNSUPPORTED -> {
+ binding.rearDisplayButton.isEnabled = false
+ binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+ }
+ WINDOW_AREA_STATUS_UNAVAILABLE -> {
+ binding.rearDisplayButton.isEnabled = false
+ binding.rearDisplayButton.text = "RearDisplay is not currently available"
+ }
+ WINDOW_AREA_STATUS_AVAILABLE -> {
+ binding.rearDisplayButton.isEnabled = true
+ binding.rearDisplayButton.text = "Enable RearDisplay Mode"
+ }
+ else -> {
+ binding.rearDisplayButton.isEnabled = false
+ binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+ }
}
}
}
@@ -161,11 +174,6 @@
return currentDate.toString()
}
- private fun getRearDisplayStatus(windowAreaInfo: WindowAreaInfo?): WindowAreaCapability.Status {
- val status = windowAreaInfo?.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)?.status
- return status ?: WINDOW_AREA_STATUS_UNSUPPORTED
- }
-
private companion object {
private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
new file mode 100644
index 0000000..c5e5985
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2023 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.demo.area
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.area.WindowAreaCapability
+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaInfo
+import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
+import androidx.window.area.WindowAreaPresentationSessionCallback
+import androidx.window.area.WindowAreaSessionPresenter
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.common.infolog.InfoLogAdapter
+import androidx.window.demo.databinding.ActivityRearDisplayPresentationBinding
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/**
+ * Demo Activity that showcases listening for the status of the [OPERATION_PRESENT_ON_AREA]
+ * operation on a [WindowAreaInfo] of type [TYPE_REAR_FACING] as well as enabling/disabling a
+ * presentation session on that window area. This Activity implements
+ * [WindowAreaPresentationSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class RearDisplayPresentationActivity :
+ EdgeToEdgeActivity(), WindowAreaPresentationSessionCallback {
+
+ private var activePresentation: WindowAreaSessionPresenter? = null
+ private var currentWindowAreaInfo: WindowAreaInfo? = null
+ private lateinit var windowAreaController: WindowAreaController
+ private val infoLogAdapter = InfoLogAdapter()
+
+ private lateinit var binding: ActivityRearDisplayPresentationBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ windowAreaController = WindowAreaController.getOrCreate()
+
+ binding = ActivityRearDisplayPresentationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Safely collect from windowInfoRepo when the lifecycle is STARTED
+ // and stops collection when the lifecycle is STOPPED
+ windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "number of areas: " + windowAreaInfos.size
+ )
+ windowAreaInfos.forEach { windowAreaInfo ->
+ if (windowAreaInfo.type == TYPE_REAR_FACING) {
+ currentWindowAreaInfo = windowAreaInfo
+ val presentCapability =
+ windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA)
+ infoLogAdapter.append(
+ getCurrentTimeString(),
+ presentCapability.status.toString() +
+ " : " +
+ windowAreaInfo.metrics.toString()
+ )
+ updateRearDisplayPresentationButton()
+ }
+ }
+ infoLogAdapter.notifyDataSetChanged()
+ }
+ }
+ }
+
+ binding.rearDisplayPresentationButton.setOnClickListener {
+ if (activePresentation != null) {
+ activePresentation?.close()
+ } else {
+ currentWindowAreaInfo?.let {
+ windowAreaController.presentContentOnWindowArea(
+ it.token,
+ this@RearDisplayPresentationActivity,
+ { obj: Runnable -> obj.run() },
+ this@RearDisplayPresentationActivity
+ )
+ }
+ }
+ }
+ }
+
+ override fun onSessionStarted(session: WindowAreaSessionPresenter) {
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "Presentation session has been started"
+ )
+
+ activePresentation = session
+ val concurrentContext: Context = session.context
+ val contentView =
+ LayoutInflater.from(concurrentContext).inflate(R.layout.concurrent_presentation, null)
+ session.setContentView(contentView)
+ activePresentation = session
+ updateRearDisplayPresentationButton()
+ }
+
+ override fun onContainerVisibilityChanged(isVisible: Boolean) {
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "Presentation content is visible: $isVisible"
+ )
+ }
+
+ override fun onSessionEnded(t: Throwable?) {
+ infoLogAdapter.appendAndNotify(
+ getCurrentTimeString(),
+ "Presentation session has been ended"
+ )
+ activePresentation = null
+ }
+
+ private fun updateRearDisplayPresentationButton() {
+ if (activePresentation != null) {
+ binding.rearDisplayPresentationButton.isEnabled = true
+ binding.rearDisplayPresentationButton.text = "Disable rear display presentation"
+ return
+ }
+ when (currentWindowAreaInfo?.getCapability(OPERATION_PRESENT_ON_AREA)?.status) {
+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
+ binding.rearDisplayPresentationButton.isEnabled = false
+ binding.rearDisplayPresentationButton.text =
+ "Rear display presentation mode is not supported on this device"
+ }
+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
+ binding.rearDisplayPresentationButton.isEnabled = false
+ binding.rearDisplayPresentationButton.text =
+ "Rear display presentation is not currently available"
+ }
+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
+ binding.rearDisplayPresentationButton.isEnabled = true
+ binding.rearDisplayPresentationButton.text = "Enable rear display presentation mode"
+ }
+ }
+ }
+
+ private fun getCurrentTimeString(): String {
+ val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+ val currentDate = sdf.format(Date())
+ return currentDate.toString()
+ }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
index 97d7ee7..5e71bee 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
@@ -25,17 +25,23 @@
import android.os.Handler
import android.os.Looper
import android.view.Display.DEFAULT_DISPLAY
-import androidx.appcompat.app.AppCompatActivity
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.demo.databinding.ActivityCoresdkWindowStateCallbackLayoutBinding
+import androidx.window.demo.R
+import androidx.window.demo.common.DemoTheme
import androidx.window.layout.WindowInfoTracker
+import androidx.window.layout.WindowMetricsCalculator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/** Activity to show display configuration from different system callbacks. */
-class WindowStateCallbackActivity : AppCompatActivity() {
+class WindowStateCallbackActivity : ComponentActivity() {
+ private val viewModel: WindowStateViewModel by viewModels()
/**
* [DisplayManager]s from `Activity` and `Application` are updated from different resource
@@ -43,15 +49,9 @@
*/
private lateinit var applicationDisplayManager: DisplayManager
private lateinit var activityDisplayManager: DisplayManager
+ private lateinit var windowMetricsCalculator: WindowMetricsCalculator
private lateinit var handler: Handler
- private lateinit var latestUpdateView: WindowStateView
- private lateinit var applicationDisplayListenerView: WindowStateView
- private lateinit var activityDisplayListenerView: WindowStateView
- private lateinit var applicationConfigurationView: WindowStateView
- private lateinit var activityConfigurationView: WindowStateView
- private lateinit var displayFeatureView: WindowStateView
-
/**
* Runnable to poll configuration every 500ms. To always provide an up-to-date configuration so
* it can be used to verify the configurations from other callbacks.
@@ -59,7 +59,7 @@
private val updateWindowStateIfChanged =
object : Runnable {
override fun run() {
- latestUpdateView.onWindowStateCallbackInvoked()
+ provideLatestWindowState()
handler.postDelayed(this, 500)
}
}
@@ -72,9 +72,10 @@
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
- if (displayId == DEFAULT_DISPLAY) {
- applicationDisplayListenerView.onWindowStateCallbackInvoked()
+ if (displayId != DEFAULT_DISPLAY) {
+ return
}
+ onWindowStateCallbackInvoked(R.string.application_display_listener_title, displayId)
}
}
@@ -86,36 +87,38 @@
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
- if (displayId == DEFAULT_DISPLAY) {
- activityDisplayListenerView.onWindowStateCallbackInvoked()
+ if (displayId != DEFAULT_DISPLAY) {
+ return
}
+ onWindowStateCallbackInvoked(R.string.activity_display_listener_title, displayId)
}
}
/** [onConfigurationChanged] on `Application`. */
private val applicationComponentCallback =
object : ComponentCallbacks {
- override fun onConfigurationChanged(p0: Configuration) {
- applicationConfigurationView.onWindowStateCallbackInvoked()
+ override fun onConfigurationChanged(configuration: Configuration) {
+ onWindowStateCallbackInvoked(
+ R.string.application_configuration_title,
+ configuration
+ )
}
+ @Deprecated(
+ "Since API level 34 this is never called. Apps targeting API level 34 " +
+ "and above may provide an empty implementation."
+ )
override fun onLowMemory() {}
}
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
- val viewBinding = ActivityCoresdkWindowStateCallbackLayoutBinding.inflate(layoutInflater)
- setContentView(viewBinding.root)
- latestUpdateView = viewBinding.latestUpdateView
- applicationDisplayListenerView = viewBinding.applicationDisplayListenerView
- activityDisplayListenerView = viewBinding.activityDisplayListenerView
- applicationConfigurationView = viewBinding.applicationConfigurationView
- activityConfigurationView = viewBinding.activityConfigurationView
- displayFeatureView = viewBinding.displayFeatureView
+ setContent { DemoTheme { WindowStateScreen() } }
- applicationDisplayManager =
- application.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
- activityDisplayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ applicationDisplayManager = application.getSystemService(DisplayManager::class.java)
+ activityDisplayManager = getSystemService(DisplayManager::class.java)
+ windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
handler = Handler(Looper.getMainLooper())
applicationDisplayManager.registerDisplayListener(applicationDisplayListener, handler)
@@ -126,7 +129,9 @@
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@WindowStateCallbackActivity)
.windowLayoutInfo(this@WindowStateCallbackActivity)
- .collect { _ -> displayFeatureView.onWindowStateCallbackInvoked() }
+ .collect { info ->
+ onWindowStateCallbackInvoked(R.string.display_feature_title, info)
+ }
}
}
}
@@ -152,10 +157,35 @@
/** [onConfigurationChanged] on `Activity`. */
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- activityConfigurationView.onWindowStateCallbackInvoked()
+ onWindowStateCallbackInvoked(R.string.activity_configuration_title, newConfig)
}
- companion object {
- val TAG = WindowStateCallbackActivity::class.simpleName
+ /** Called when the corresponding system callback is invoked. */
+ private fun onWindowStateCallbackInvoked(resId: Int, details: Any?) {
+ viewModel.onWindowStateCallback(queryWindowState(resId, details = "$details"))
+ }
+
+ private fun provideLatestWindowState() {
+ viewModel.updateLatestWindowState(
+ queryWindowState(
+ R.string.latest_configuration_title,
+ "poll configuration every 500ms",
+ )
+ )
+ }
+
+ private fun queryWindowState(resId: Int, details: String): WindowState {
+ fun DisplayManager.defaultDisplayRotation() = getDisplay(DEFAULT_DISPLAY).rotation
+ fun Context.displayBounds() =
+ windowMetricsCalculator.computeMaximumWindowMetrics(this).bounds
+
+ return WindowState(
+ name = getString(resId),
+ applicationDisplayRotation = applicationDisplayManager.defaultDisplayRotation(),
+ activityDisplayRotation = activityDisplayManager.defaultDisplayRotation(),
+ applicationDisplayBounds = applicationContext.displayBounds(),
+ activityDisplayBounds = [email protected](),
+ callbackDetails = details,
+ )
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt
deleted file mode 100644
index 639491f..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2023 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.demo.coresdk
-
-import android.content.Context
-import android.graphics.Color
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.window.demo.R
-import androidx.window.demo.databinding.WindowStateConfigViewBinding
-
-/** View to show a display configuration value. */
-class WindowStateConfigView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0
-) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
-
- private val configView: TextView
- private var configValue: String? = null
-
- /** Whether to highlight the value when it is changed. */
- var shouldHighlightChange = false
-
- init {
- val viewBinding =
- WindowStateConfigViewBinding.inflate(LayoutInflater.from(context), this, true)
- configView = viewBinding.configValue
- context.theme.obtainStyledAttributes(attrs, R.styleable.WindowStateConfigView, 0, 0).apply {
- try {
- getString(R.styleable.WindowStateConfigView_configName)?.let {
- viewBinding.configName.text = it
- }
- } finally {
- recycle()
- }
- }
- }
-
- /** Updates the config value. */
- fun updateValue(value: String) {
- if (shouldHighlightChange && configValue != null && configValue != value) {
- // Highlight previous value if changed.
- configView.setTextColor(Color.RED)
- } else {
- configView.setTextColor(Color.BLACK)
- }
- configValue = value
- configView.text = value
- }
-}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
new file mode 100644
index 0000000..92e85eb
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2024 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.demo.coresdk
+
+import android.graphics.Rect
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_180
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.window.demo.R
+import androidx.window.demo.common.DemoTheme
+import java.text.SimpleDateFormat
+import java.util.Date
+import kotlinx.coroutines.launch
+
+/** Composes the main screen for displaying window state information. */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun WindowStateScreen(viewModel: WindowStateViewModel = viewModel()) {
+ val windowStates by viewModel.windowStates.collectAsState()
+ val listState = rememberLazyListState()
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Window State Callbacks") },
+ actions = {
+ IconButton(onClick = viewModel::clearWindowStates) {
+ Icon(Icons.Filled.Delete, "Clear list")
+ }
+ },
+ colors =
+ TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ titleContentColor = Color.White,
+ actionIconContentColor = Color.White,
+ ),
+ )
+ },
+ floatingActionButton = {
+ val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
+ if (showButton) {
+ val coroutineScope = rememberCoroutineScope()
+ FloatingActionButton(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = Color.White,
+ onClick = { coroutineScope.launch { listState.scrollToItem(0) } },
+ ) {
+ Text("Back to latest!", modifier = Modifier.padding(8.dp))
+ }
+ }
+ },
+ floatingActionButtonPosition = FabPosition.Center,
+ ) { padding ->
+ WindowStateList(
+ windowStates,
+ padding,
+ listState = listState,
+ onWindowStateItemClick = viewModel::onWindowStateItemClick,
+ )
+ }
+}
+
+/** Previews composable for the [WindowStateScreen] in both light and dark modes. */
+@PreviewLightDark
+@Composable
+fun WindowStateScreenPreview() {
+ val windowStates =
+ listOf(
+ WindowState(
+ name = stringResource(R.string.application_configuration_title),
+ applicationDisplayRotation = ROTATION_270,
+ activityDisplayRotation = ROTATION_270,
+ applicationDisplayBounds = Rect(0, 0, 2208, 1840),
+ activityDisplayBounds = Rect(0, 0, 2208, 1840),
+ ),
+ WindowState(
+ name = stringResource(R.string.activity_display_listener_title),
+ applicationDisplayRotation = ROTATION_270,
+ activityDisplayRotation = ROTATION_270,
+ applicationDisplayBounds = Rect(0, 0, 2208, 1840),
+ activityDisplayBounds = Rect(0, 0, 2208, 1840),
+ ),
+ WindowState(
+ name = stringResource(R.string.display_feature_title),
+ applicationDisplayBounds = Rect(0, 0, 960, 2142),
+ activityDisplayBounds = Rect(0, 0, 960, 2142),
+ ),
+ WindowState(
+ name = stringResource(R.string.latest_configuration_title),
+ applicationDisplayBounds = Rect(0, 0, 960, 2142),
+ activityDisplayBounds = Rect(0, 0, 960, 2142),
+ ),
+ )
+ DemoTheme { WindowStateScreen(viewModel = WindowStateViewModel(windowStates)) }
+}
+
+/**
+ * Composes a scrollable list of [WindowStateCard] items.
+ *
+ * @param windowStates list of [WindowState] objects to display.
+ * @param contentPadding padding to apply to the lazy list.
+ * @param listState state object for the lazy list.
+ * @param onWindowStateItemClick callback when a [WindowState] item is clicked.
+ */
+@Composable
+fun WindowStateList(
+ windowStates: List<WindowState>,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ listState: LazyListState = rememberLazyListState(),
+ onWindowStateItemClick: (Int) -> Unit,
+) {
+ LazyColumn(
+ contentPadding = contentPadding,
+ state = listState,
+ ) {
+ itemsIndexed(windowStates) { index, state ->
+ WindowStateCard(
+ number = windowStates.size - index,
+ state = state,
+ lastState = windowStates.getOrNull(index + 1) ?: state,
+ toggleExpand = { onWindowStateItemClick(index) },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+}
+
+/**
+ * Composes a card displaying information about a [WindowState].
+ *
+ * @param number the number to display for this [WindowState].
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ * @param toggleExpand callback to toggle the expanded state of the card.
+ * @param modifier modifier for this card.
+ */
+@Composable
+private fun WindowStateCard(
+ number: Int,
+ state: WindowState,
+ lastState: WindowState,
+ toggleExpand: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isExpanded = state.isDetailsExpanded
+ Card(modifier.padding(vertical = 4.dp, horizontal = 8.dp)) {
+ Row(
+ modifier =
+ Modifier.padding(horizontal = 8.dp)
+ .animateContentSize(
+ animationSpec =
+ spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Row(modifier = Modifier.weight(0.85f), verticalAlignment = Alignment.CenterVertically) {
+ Text("#$number")
+ Spacer(modifier = Modifier.width(6.dp))
+ Column {
+ if (!isExpanded) {
+ WindowStateSummary(state, lastState)
+ } else {
+ WindowStateDetail(state, lastState)
+ }
+ }
+ }
+ IconButton(onClick = toggleExpand, modifier = Modifier.weight(0.07f)) {
+ Icon(
+ if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
+ contentDescription = if (isExpanded) "Show less" else "Show more",
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Composes a summary view for a collapsed [WindowStateCard].
+ *
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ */
+@Composable
+private fun WindowStateSummary(state: WindowState, lastState: WindowState) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ state.name,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ state.timeStamp.toSimpleTimeStr(),
+ style = MaterialTheme.typography.bodySmall,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ }
+ Text(
+ buildAnnotatedString {
+ rotationString(
+ state.applicationDisplayRotation,
+ highlight =
+ state.applicationDisplayRotation != lastState.applicationDisplayRotation,
+ )
+ append(" / ")
+ rotationString(
+ state.activityDisplayRotation,
+ highlight = state.activityDisplayRotation != lastState.activityDisplayRotation,
+ )
+ append(" / ")
+ boundsString(
+ state.applicationDisplayBounds,
+ highlight = state.applicationDisplayBounds != lastState.applicationDisplayBounds,
+ )
+ append(" / ")
+ boundsString(
+ state.activityDisplayBounds,
+ highlight = state.activityDisplayBounds != lastState.activityDisplayBounds,
+ )
+ },
+ style = MaterialTheme.typography.bodySmall,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+}
+
+/**
+ * Composes a detailed view for an expanded [WindowStateCard].
+ *
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ */
+@Composable
+private fun WindowStateDetail(state: WindowState, lastState: WindowState) {
+ val timestampTitle = stringResource(R.string.timestamp_title)
+ val applicationRotationTitle = stringResource(R.string.application_display_rotation_title)
+ val activityRotationTitle = stringResource(R.string.activity_display_rotation_title)
+ val applicationBoundsTittle = stringResource(R.string.application_display_bounds_title)
+ val activityBoundsTittle = stringResource(R.string.activity_display_bounds_title)
+
+ Text(
+ state.name,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ )
+ Column {
+ Text(
+ "$timestampTitle ${state.timeStamp}",
+ style = MaterialTheme.typography.bodySmall,
+ )
+ DisplayRotationView(
+ applicationRotationTitle,
+ currentRotation = state.applicationDisplayRotation,
+ lastRotation = lastState.applicationDisplayRotation,
+ )
+ DisplayRotationView(
+ activityRotationTitle,
+ currentRotation = state.activityDisplayRotation,
+ lastRotation = lastState.activityDisplayRotation,
+ )
+ DisplayBoundsView(
+ applicationBoundsTittle,
+ currentBounds = state.applicationDisplayBounds,
+ lastBound = lastState.applicationDisplayBounds,
+ )
+ DisplayBoundsView(
+ activityBoundsTittle,
+ currentBounds = state.activityDisplayBounds,
+ lastBound = lastState.activityDisplayBounds,
+ )
+ Text(
+ "Callback details: ${state.callbackDetails}",
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+}
+
+/**
+ * Composes a view for displaying rotation information in [WindowStateDetail].
+ *
+ * @param title the title for this rotation information.
+ * @param currentRotation the current rotation value to display.
+ * @param lastRotation the previous rotation value for comparison.
+ */
+@Composable
+private fun DisplayRotationView(title: String, currentRotation: Int, lastRotation: Int) {
+ val shouldHighlightChange = currentRotation != lastRotation
+ Row {
+ Text(title, style = MaterialTheme.typography.bodySmall)
+ Text(
+ buildAnnotatedString { rotationString(currentRotation, shouldHighlightChange) },
+ style = MaterialTheme.typography.bodySmall,
+ )
+ if (shouldHighlightChange) {
+ Text(
+ text = lastRotation.toRotationStr(),
+ style =
+ MaterialTheme.typography.bodySmall.copy(
+ textDecoration = TextDecoration.LineThrough,
+ ),
+ modifier = Modifier.padding(start = 2.dp),
+ )
+ }
+ }
+}
+
+/** Formats a rotation value into an annotated string. */
+private fun AnnotatedString.Builder.rotationString(rotation: Int, highlight: Boolean) {
+ val color = if (highlight) Color.Red else Color.Unspecified
+ withStyle(style = SpanStyle(color = color)) { append(rotation.toRotationStr()) }
+}
+
+/**
+ * Composes a view for displaying bounds information in [WindowStateDetail].
+ *
+ * @param title the title for this bounds information.
+ * @param currentBounds the current bounds value to display.
+ * @param lastBound the previous bounds value for comparison.
+ */
+@Composable
+private fun DisplayBoundsView(title: String, currentBounds: Rect, lastBound: Rect) {
+ val shouldHighlightChange = currentBounds != lastBound
+ Row {
+ Text(title, style = MaterialTheme.typography.bodySmall)
+ Column {
+ Text(
+ buildAnnotatedString { boundsString(currentBounds, shouldHighlightChange) },
+ style = MaterialTheme.typography.bodySmall,
+ )
+ if (shouldHighlightChange) {
+ Text(
+ text = "$lastBound",
+ style =
+ MaterialTheme.typography.bodySmall.copy(
+ textDecoration = TextDecoration.LineThrough,
+ ),
+ )
+ }
+ }
+ }
+}
+
+/** Formats a bounds value into an annotated string. */
+private fun AnnotatedString.Builder.boundsString(bound: Rect, highlight: Boolean) {
+ val color = if (highlight) Color.Red else Color.Unspecified
+ withStyle(style = SpanStyle(color = color)) { append("$bound") }
+}
+
+/** Converts an integer rotation value to a human-readable string representation. */
+private fun Int.toRotationStr(): String =
+ when (this) {
+ ROTATION_0 -> "0°"
+ ROTATION_90 -> "90°"
+ ROTATION_180 -> "180°"
+ ROTATION_270 -> "270°"
+ else -> "Unknown"
+ }
+
+/** Converts a [Date] to a simple time string for summary view. */
+private fun Date.toSimpleTimeStr(): String = SimpleDateFormat("(HH:mm:ss)").format(this)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt
deleted file mode 100644
index a128b484..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2023 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.demo.coresdk
-
-import android.content.Context
-import android.graphics.Rect
-import android.hardware.display.DisplayManager
-import android.util.AttributeSet
-import android.util.Log
-import android.view.Display.DEFAULT_DISPLAY
-import android.view.LayoutInflater
-import android.view.WindowManager
-import android.widget.LinearLayout
-import androidx.window.demo.R
-import androidx.window.demo.databinding.WindowStateViewBinding
-import androidx.window.layout.WindowMetricsCalculator
-import java.text.SimpleDateFormat
-import java.util.Date
-
-/** View to show the display configuration from the latest update. */
-class WindowStateView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0
-) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
-
- private var title: String = "N/A"
-
- /**
- * [DisplayManager]s and [WindowManager]s from `Activity` and `Application` are updated from
- * different resource configurations, so we show config from them separately.
- */
- private val applicationDisplayManager: DisplayManager
- private val activityDisplayManager: DisplayManager
- private val windowMetricsCalculator: WindowMetricsCalculator
-
- private val timestampView: WindowStateConfigView
- private val applicationDisplayRotationView: WindowStateConfigView
- private val activityDisplayRotationView: WindowStateConfigView
- private val applicationDisplayBoundsView: WindowStateConfigView
- private val activityDisplayBoundsView: WindowStateConfigView
- private val prevApplicationDisplayRotationView: WindowStateConfigView
- private val prevActivityDisplayRotationView: WindowStateConfigView
- private val prevApplicationDisplayBoundsView: WindowStateConfigView
- private val prevActivityDisplayBoundsView: WindowStateConfigView
-
- private val shouldHidePrevConfig: Boolean
- private var lastApplicationDisplayRotation = -1
- private var lastActivityDisplayRotation = -1
- private val lastApplicationDisplayBounds = Rect()
- private val lastActivityDisplayBounds = Rect()
-
- init {
- applicationDisplayManager =
- context.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
- activityDisplayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
- windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
-
- val viewBinding = WindowStateViewBinding.inflate(LayoutInflater.from(context), this, true)
- timestampView = viewBinding.timestampView
- applicationDisplayRotationView = viewBinding.applicationDisplayRotationView
- activityDisplayRotationView = viewBinding.activityDisplayRotationView
- applicationDisplayBoundsView = viewBinding.applicationDisplayBoundsView
- activityDisplayBoundsView = viewBinding.activityDisplayBoundsView
- prevApplicationDisplayRotationView = viewBinding.prevApplicationDisplayRotationView
- prevActivityDisplayRotationView = viewBinding.prevActivityDisplayRotationView
- prevApplicationDisplayBoundsView = viewBinding.prevApplicationDisplayBoundsView
- prevActivityDisplayBoundsView = viewBinding.prevActivityDisplayBoundsView
-
- context.theme.obtainStyledAttributes(attrs, R.styleable.WindowStateView, 0, 0).apply {
- try {
- getString(R.styleable.WindowStateView_title)?.let {
- viewBinding.callbackTitle.text = it
- title = it
- }
- shouldHidePrevConfig = getBoolean(R.styleable.WindowStateView_hidePrevConfig, false)
- if (shouldHidePrevConfig) {
- timestampView.visibility = GONE
- applicationDisplayRotationView.shouldHighlightChange = false
- activityDisplayRotationView.shouldHighlightChange = false
- applicationDisplayBoundsView.shouldHighlightChange = false
- activityDisplayBoundsView.shouldHighlightChange = false
- prevApplicationDisplayRotationView.visibility = GONE
- prevActivityDisplayRotationView.visibility = GONE
- prevApplicationDisplayBoundsView.visibility = GONE
- prevActivityDisplayBoundsView.visibility = GONE
- } else {
- applicationDisplayRotationView.shouldHighlightChange = true
- activityDisplayRotationView.shouldHighlightChange = true
- applicationDisplayBoundsView.shouldHighlightChange = true
- activityDisplayBoundsView.shouldHighlightChange = true
- }
- } finally {
- recycle()
- }
- }
- }
-
- /** Called when the corresponding system callback is invoked. */
- fun onWindowStateCallbackInvoked() {
- val applicationDisplayRotation =
- applicationDisplayManager.getDisplay(DEFAULT_DISPLAY).rotation
- val activityDisplayRotation = activityDisplayManager.getDisplay(DEFAULT_DISPLAY).rotation
- val applicationDisplayBounds =
- windowMetricsCalculator.computeMaximumWindowMetrics(context.applicationContext).bounds
- val activityDisplayBounds =
- windowMetricsCalculator.computeMaximumWindowMetrics(context).bounds
-
- if (
- shouldHidePrevConfig &&
- applicationDisplayRotation == lastApplicationDisplayRotation &&
- activityDisplayRotation == lastActivityDisplayRotation &&
- applicationDisplayBounds == lastApplicationDisplayBounds &&
- activityDisplayBounds == lastActivityDisplayBounds
- ) {
- // Skip if the state is unchanged.
- return
- }
-
- if (!shouldHidePrevConfig) {
- // Debug log for the change title.
- Log.d(WindowStateCallbackActivity.TAG, title)
- }
-
- timestampView.updateValue(TIME_FORMAT.format(Date()))
- applicationDisplayRotationView.updateValue(applicationDisplayRotation.toString())
- activityDisplayRotationView.updateValue(activityDisplayRotation.toString())
- applicationDisplayBoundsView.updateValue(applicationDisplayBounds.toString())
- activityDisplayBoundsView.updateValue(activityDisplayBounds.toString())
-
- if (!shouldHidePrevConfig && lastApplicationDisplayRotation != -1) {
- // Skip if there is no previous value.
- prevApplicationDisplayRotationView.updateValue(
- lastApplicationDisplayRotation.toString()
- )
- prevActivityDisplayRotationView.updateValue(lastActivityDisplayRotation.toString())
- prevApplicationDisplayBoundsView.updateValue(lastApplicationDisplayBounds.toString())
- prevActivityDisplayBoundsView.updateValue(lastActivityDisplayBounds.toString())
- }
-
- lastApplicationDisplayRotation = applicationDisplayRotation
- lastActivityDisplayRotation = activityDisplayRotation
- lastApplicationDisplayBounds.set(applicationDisplayBounds)
- lastActivityDisplayBounds.set(activityDisplayBounds)
- }
-
- companion object {
- private val TIME_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
- }
-}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt
new file mode 100644
index 0000000..3ecab868
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 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.demo.coresdk
+
+import android.graphics.Rect
+import android.view.Surface.ROTATION_0
+import androidx.compose.runtime.Immutable
+import androidx.lifecycle.ViewModel
+import java.util.Date
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** Represents the display configuration, including rotations, bounds, and callback source. */
+@Immutable
+data class WindowState(
+ val name: String,
+ val timeStamp: Date = Date(),
+ val applicationDisplayRotation: Int = ROTATION_0,
+ val activityDisplayRotation: Int = ROTATION_0,
+ val applicationDisplayBounds: Rect = Rect(),
+ val activityDisplayBounds: Rect = Rect(),
+ val callbackDetails: String = "",
+ val isDetailsExpanded: Boolean = false,
+) {
+ /**
+ * Compares the core state properties of this WindowState with another object.
+ *
+ * @param other the object to compare with.
+ * @return `true` if the core state properties are the same, `false` otherwise.
+ */
+ fun isSameState(other: Any?): Boolean =
+ (other is WindowState) &&
+ applicationDisplayRotation == other.applicationDisplayRotation &&
+ activityDisplayRotation == other.activityDisplayRotation &&
+ applicationDisplayBounds == other.applicationDisplayBounds &&
+ activityDisplayBounds == other.activityDisplayBounds
+}
+
+/**
+ * Manages the state of window events.
+ *
+ * This ViewModel maintains a list of [WindowState] objects, representing different window
+ * configurations over time. It provides methods to add new states, update existing ones, clear the
+ * list, and handle user interactions with the UI.
+ *
+ * @param initStates an optional initial list of [WindowState] objects to populate the ViewModel
+ * with upon creation. Defaults to an empty list.
+ * @property windowStates the current list of window states, this is exposed for the UI to observe.
+ */
+class WindowStateViewModel(initStates: List<WindowState> = emptyList()) : ViewModel() {
+ private val _states = MutableStateFlow(initStates)
+ val windowStates: StateFlow<List<WindowState>> = _states.asStateFlow()
+
+ /** Clears all window states from the list. */
+ fun clearWindowStates() = _states.update { emptyList() }
+
+ /** Adds a new window state to the beginning of the list. */
+ fun onWindowStateCallback(state: WindowState) =
+ _states.update { listOf(state) + it } // Prepend to the list.
+
+ /** Updates the latest window state if it's different from the current latest state. */
+ fun updateLatestWindowState(state: WindowState) =
+ _states.update { if (state.isSameState(it.firstOrNull())) it else listOf(state) + it }
+
+ /** Toggles the expanded state of a window state at the given index. */
+ fun onWindowStateItemClick(index: Int) =
+ _states.update { states -> states.updateAtIndex(index) { it.toggleExpand() } }
+}
+
+/**
+ * Toggles the expanded state of a WindowState.
+ *
+ * @return a new [WindowState] with the `isDetailsExpanded` property toggled.
+ */
+private fun WindowState.toggleExpand(): WindowState = copy(isDetailsExpanded = !isDetailsExpanded)
+
+/**
+ * Updates an item at a specific index in a list using the provided transform function.
+ *
+ * @param index the index of the item to update.
+ * @param transform the function to apply to the item at the specified index.
+ * @return a new list with the item at the specified index updated.
+ */
+private fun <T> List<T>.updateAtIndex(index: Int, transform: (T) -> T): List<T> =
+ mapIndexed { idx, value ->
+ if (idx == index) transform(value) else value
+ }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
index c915ee8..83ac0b2 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
@@ -17,7 +17,6 @@
package androidx.window.demo.demos
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.window.demo.DisplayFeaturesLetterboxLandscapeSlimActivity
import androidx.window.demo.DisplayFeaturesLetterboxPortraitSlimActivity
@@ -35,10 +34,12 @@
import androidx.window.demo.R.string.show_all_display_features_portrait_slim
import androidx.window.demo.SplitLayoutActivity
import androidx.window.demo.WindowMetricsActivity
+import androidx.window.demo.area.RearDisplayPresentationActivity
import androidx.window.demo.common.DisplayFeaturesActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
/** Main activity that launches WindowManager demos. */
-class WindowDemosActivity : AppCompatActivity() {
+class WindowDemosActivity : EdgeToEdgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -84,6 +85,11 @@
buttonTitle = getString(R.string.ime),
description = getString(R.string.ime_demo_description),
clazz = ImeActivity::class.java
+ ),
+ DemoItem(
+ buttonTitle = getString(R.string.dual_display),
+ description = getString(R.string.dual_display_description),
+ clazz = RearDisplayPresentationActivity::class.java
)
)
val recyclerView = findViewById<RecyclerView>(R.id.demo_recycler_view)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
index 7f1e0a7..b5b677a 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
@@ -16,9 +16,15 @@
package androidx.window.demo.embedding
+import android.graphics.Color
import androidx.annotation.GuardedBy
+import androidx.window.demo.embedding.OverlayActivityBase.Companion.DEFAULT_OVERLAY_ATTRIBUTES
+import androidx.window.embedding.EmbeddingAnimationBackground
+import androidx.window.embedding.EmbeddingAnimationParams
+import androidx.window.embedding.OverlayAttributes
import androidx.window.embedding.SplitAttributes
import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -55,6 +61,71 @@
@GuardedBy("lock") private var splitTypeLocked = SplitAttributes.SplitType.SPLIT_TYPE_EQUAL
+ @GuardedBy("lock") private var animationBackgroundLocked = EmbeddingAnimationBackground.DEFAULT
+
+ @GuardedBy("lock")
+ private var openAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+ @GuardedBy("lock")
+ private var closeAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+ @GuardedBy("lock")
+ private var changeAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+ internal var animationBackground: EmbeddingAnimationBackground
+ get() {
+ lock.withLock {
+ return animationBackgroundLocked
+ }
+ }
+ set(value) {
+ lock.withLock { animationBackgroundLocked = value }
+ }
+
+ internal var openAnimation: EmbeddingAnimationParams.AnimationSpec
+ get() {
+ lock.withLock {
+ return openAnimationLocked
+ }
+ }
+ set(value) {
+ lock.withLock { openAnimationLocked = value }
+ }
+
+ internal var closeAnimation: EmbeddingAnimationParams.AnimationSpec
+ get() {
+ lock.withLock {
+ return closeAnimationLocked
+ }
+ }
+ set(value) {
+ lock.withLock { closeAnimationLocked = value }
+ }
+
+ internal var changeAnimation: EmbeddingAnimationParams.AnimationSpec
+ get() {
+ lock.withLock {
+ return changeAnimationLocked
+ }
+ }
+ set(value) {
+ lock.withLock { changeAnimationLocked = value }
+ }
+
+ internal var overlayAttributes: OverlayAttributes
+ get() {
+ lock.withLock {
+ return overlayAttributesLocked
+ }
+ }
+ set(value) {
+ lock.withLock { overlayAttributesLocked = value }
+ }
+
+ @GuardedBy("lock") private var overlayAttributesLocked = DEFAULT_OVERLAY_ATTRIBUTES
+
+ internal var overlayMode = AtomicInteger()
+
companion object {
@Volatile private var globalInstance: DemoActivityEmbeddingController? = null
private val globalLock = ReentrantLock()
@@ -71,5 +142,23 @@
}
return globalInstance!!
}
+
+ /** Animation background constants. */
+ val ANIMATION_BACKGROUND_TEXTS = arrayOf("DEFAULT", "BLUE", "GREEN", "YELLOW")
+ val ANIMATION_BACKGROUND_VALUES =
+ arrayOf(
+ EmbeddingAnimationBackground.DEFAULT,
+ EmbeddingAnimationBackground.createColorBackground(Color.BLUE),
+ EmbeddingAnimationBackground.createColorBackground(Color.GREEN),
+ EmbeddingAnimationBackground.createColorBackground(Color.YELLOW)
+ )
+
+ /** Animation spec constants. */
+ val ANIMATION_SPEC_TEXTS = arrayOf("DEFAULT", "JUMP_CUT")
+ val ANIMATION_SPEC_VALUES =
+ arrayOf(
+ EmbeddingAnimationParams.AnimationSpec.DEFAULT,
+ EmbeddingAnimationParams.AnimationSpec.JUMP_CUT
+ )
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
new file mode 100644
index 0000000..ef31132
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 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.demo.embedding
+
+import androidx.window.demo.common.EdgeToEdgeActivity
+
+/** Dialog style Activity. */
+open class DialogActivity : EdgeToEdgeActivity()
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index 0c2288e..235558f 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -20,6 +20,10 @@
import androidx.startup.Initializer
import androidx.window.WindowSdkExtensions
import androidx.window.demo.R
+import androidx.window.demo.embedding.OverlayActivityBase.Companion.OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_FULLSCREEN_TOGGLE
import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_PLACEHOLDER
import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.TAG_CUSTOMIZED_SPLIT_ATTRIBUTES
@@ -31,6 +35,15 @@
import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP
import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING
import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_USE_DEFAULT_SPLIT_ATTRIBUTES
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.EmbeddingAnimationParams
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingConfiguration
+import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_TASK
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayAttributesCalculatorParams
+import androidx.window.embedding.OverlayController
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
@@ -50,16 +63,31 @@
/** Initializes SplitController with a set of statically defined rules. */
class ExampleWindowInitializer : Initializer<RuleController> {
- private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+ private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+ private lateinit var splitController: SplitController
+
+ private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
override fun create(context: Context): RuleController {
- SplitController.getInstance(context).apply {
- if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
- setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
+ splitController = SplitController.getInstance(context)
+
+ if (extensionVersion >= 2) {
+ splitController.setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
+ }
+ if (extensionVersion >= OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION) {
+ OverlayController.getInstance(context)
+ .setOverlayAttributesCalculator(::sampleOverlayAttributesCalculator)
+ }
+ ActivityEmbeddingController.getInstance(context).apply {
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
+ setEmbeddingConfiguration(
+ EmbeddingConfiguration.Builder().setDimAreaBehavior(ON_TASK).build()
+ )
}
}
return RuleController.getInstance(context).apply {
- if (SplitController.getInstance(context).splitSupportStatus == SPLIT_AVAILABLE) {
+ if (splitController.splitSupportStatus == SPLIT_AVAILABLE) {
setRules(RuleController.parseRules(context, R.xml.main_split_config))
}
}
@@ -79,7 +107,7 @@
SplitAttributes.Builder().setSplitType(SPLIT_TYPE_EXPAND).build()
if (
tag?.startsWith(PREFIX_FULLSCREEN_TOGGLE) == true &&
- mDemoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
+ demoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
) {
return expandContainersAttrs
}
@@ -89,12 +117,22 @@
val isBookMode = windowLayoutInfo.isBookMode()
val config = params.parentConfiguration
val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
- // Make a copy of the default splitAttributes, but replace the animation background
- // color to what is configured in the Demo app.
+ // Always use the Demo app specified animation background.
+ val animationParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+ .setDemoAppAnimations(params.defaultSplitAttributes.animationParams)
+ .build()
val defaultSplitAttributes =
SplitAttributes.Builder()
.setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
.setSplitType(params.defaultSplitAttributes.splitType)
+ .setAnimationParams(animationParams)
+ .apply {
+ if (extensionVersion >= 6) {
+ setDividerAttributes(params.defaultSplitAttributes.dividerAttributes)
+ }
+ }
.build()
when (
tag?.removePrefix(PREFIX_FULLSCREEN_TOGGLE)
@@ -125,6 +163,7 @@
TOP_TO_BOTTOM
}
)
+ .setAnimationParams(animationParams)
.build()
} else if (isPortrait) {
return expandContainersAttrs
@@ -141,6 +180,7 @@
TOP_TO_BOTTOM
}
)
+ .setAnimationParams(animationParams)
.build()
}
}
@@ -155,6 +195,7 @@
TOP_TO_BOTTOM
}
)
+ .setAnimationParams(animationParams)
.build()
} else {
SplitAttributes.Builder()
@@ -166,6 +207,7 @@
LEFT_TO_RIGHT
}
)
+ .setAnimationParams(animationParams)
.build()
}
}
@@ -182,6 +224,7 @@
TOP_TO_BOTTOM
}
)
+ .setAnimationParams(animationParams)
.build()
} else {
SplitAttributes.Builder()
@@ -193,6 +236,7 @@
LEFT_TO_RIGHT
}
)
+ .setAnimationParams(animationParams)
.build()
}
}
@@ -214,19 +258,72 @@
if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
}
)
+ .setAnimationParams(animationParams)
.build()
}
}
TAG_CUSTOMIZED_SPLIT_ATTRIBUTES -> {
return SplitAttributes.Builder()
- .setSplitType(mDemoActivityEmbeddingController.customizedSplitType)
- .setLayoutDirection(mDemoActivityEmbeddingController.customizedLayoutDirection)
+ .setSplitType(demoActivityEmbeddingController.customizedSplitType)
+ .setLayoutDirection(demoActivityEmbeddingController.customizedLayoutDirection)
+ .setAnimationParams(animationParams)
.build()
}
}
return defaultSplitAttributes
}
+ private fun EmbeddingAnimationParams.Builder.setDemoAppAnimations(
+ params: EmbeddingAnimationParams
+ ): EmbeddingAnimationParams.Builder {
+ // Replace the transition animations with what is configured in the Demo app if the default
+ // splitAttributes' transition animations are applicable (all configured to default).
+ val useDemoAppAnimations =
+ params.openAnimation == AnimationSpec.DEFAULT &&
+ params.closeAnimation == AnimationSpec.DEFAULT &&
+ params.changeAnimation == AnimationSpec.DEFAULT
+ if (useDemoAppAnimations) {
+ setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+ setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+ setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+ } else {
+ setOpenAnimation(params.openAnimation)
+ setCloseAnimation(params.closeAnimation)
+ setChangeAnimation(params.changeAnimation)
+ }
+ return this
+ }
+
+ private fun sampleOverlayAttributesCalculator(
+ params: OverlayAttributesCalculatorParams
+ ): OverlayAttributes =
+ when (val mode = demoActivityEmbeddingController.overlayMode.get()) {
+ // Put the overlay to the right
+ OVERLAY_MODE_SIMPLE.value -> params.defaultOverlayAttributes
+ // Update the overlay with orientation:
+ // - Put the overlay to the bottom if the device is in portrait
+ // - Otherwise, put the overlay to the right if the device is in landscape
+ OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value ->
+ OverlayAttributes(
+ if (params.parentWindowMetrics.isPortrait()) {
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+ width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+ height = EmbeddingBounds.Dimension.ratio(0.4f)
+ )
+ } else {
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_RIGHT,
+ width = EmbeddingBounds.Dimension.ratio(0.5f),
+ height = EmbeddingBounds.Dimension.ratio(0.8f)
+ )
+ }
+ )
+ // Fully customized overlay presentation
+ OVERLAY_MODE_CUSTOMIZATION.value -> demoActivityEmbeddingController.overlayAttributes
+ else -> throw IllegalStateException("Unknown mode $mode")
+ }
+
private fun WindowMetrics.isPortrait(): Boolean = bounds.height() > bounds.width()
private fun WindowLayoutInfo.isTabletop(): Boolean {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
index 886afee..55405f6 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
@@ -18,10 +18,10 @@
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
/** Activity to show a dialog. */
-class ExpandedDialogActivity : AppCompatActivity() {
+class ExpandedDialogActivity : EdgeToEdgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
new file mode 100644
index 0000000..2010317
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2024 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.demo.embedding
+
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.RadioGroup
+import android.widget.SeekBar
+import android.widget.Spinner
+import android.widget.Toast
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivityOverlayActivityLayoutBinding
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
+import androidx.window.embedding.EmbeddingBounds.Dimension
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
+import androidx.window.embedding.SplitController
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
+import androidx.window.embedding.setLaunchingActivityStack
+import androidx.window.embedding.setOverlayCreateParams
+import kotlinx.coroutines.launch
+
+open class OverlayActivityBase :
+ EdgeToEdgeActivity(),
+ View.OnClickListener,
+ RadioGroup.OnCheckedChangeListener,
+ AdapterView.OnItemSelectedListener,
+ SeekBar.OnSeekBarChangeListener {
+
+ private val overlayTag = OverlayCreateParams.generateOverlayTag()
+
+ private lateinit var splitController: SplitController
+
+ private lateinit var overlayController: OverlayController
+
+ private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+ private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
+
+ lateinit var viewBinding: ActivityOverlayActivityLayoutBinding
+
+ private var overlayActivityStack: ActivityStack? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ viewBinding = ActivityOverlayActivityLayoutBinding.inflate(layoutInflater)
+ splitController = SplitController.getInstance(this)
+ overlayController = OverlayController.getInstance(this)
+
+ if (
+ splitController.splitSupportStatus != SPLIT_AVAILABLE ||
+ extensionVersion < OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION
+ ) {
+ Toast.makeText(this, R.string.toast_show_overlay_warning, Toast.LENGTH_SHORT).show()
+ finish()
+ return
+ }
+
+ viewBinding.root.setBackgroundColor(Color.parseColor("#fff3e0"))
+ setContentView(viewBinding.root)
+
+ viewBinding.buttonUpdateOverlayLayout.setOnClickListener(this)
+ viewBinding.buttonLaunchOverlayContainer.setOnClickListener(this)
+ viewBinding.buttonLaunchOverlayActivityA.setOnClickListener(this)
+ viewBinding.buttonLaunchOverlayActivityB.setOnClickListener(this)
+ viewBinding.buttonFinishThisActivity.setOnClickListener(this)
+
+ val radioGroupChooseOverlayLayout = viewBinding.radioGroupChooseOverlayLayout
+ radioGroupChooseOverlayLayout.setOnCheckedChangeListener(this)
+
+ viewBinding.spinnerAlignment.apply {
+ adapter =
+ ArrayAdapter(
+ this@OverlayActivityBase,
+ android.R.layout.simple_spinner_dropdown_item,
+ POSITION_TEXT_ARRAY,
+ )
+ onItemSelectedListener = this@OverlayActivityBase
+ }
+
+ val dimensionAdapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DIMENSION_TYPE_TEXT_ARRAY,
+ )
+ viewBinding.spinnerWidth.apply {
+ adapter = dimensionAdapter
+ onItemSelectedListener = this@OverlayActivityBase
+ }
+
+ viewBinding.spinnerHeight.apply {
+ adapter = dimensionAdapter
+ onItemSelectedListener = this@OverlayActivityBase
+ }
+
+ viewBinding.seekBarHeightInRatio.setOnSeekBarChangeListener(this)
+ viewBinding.seekBarWidthInRatio.setOnSeekBarChangeListener(this)
+
+ initializeUi()
+
+ lifecycleScope.launch {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ overlayController.overlayInfo(overlayTag).collect { overlayInfo ->
+ overlayActivityStack = overlayInfo.activityStack
+ val hasOverlay = overlayActivityStack != null
+ viewBinding.buttonUpdateOverlayLayout.isEnabled =
+ hasOverlay &&
+ demoActivityEmbeddingController.overlayMode.get() !=
+ OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
+ updateOverlayBoundsText(overlayInfo)
+ }
+ }
+ }
+ }
+
+ private fun initializeUi() {
+ viewBinding.buttonUpdateOverlayLayout.isEnabled = false
+ viewBinding.radioGroupChooseOverlayLayout.check(R.id.radioButton_simple_overlay)
+ viewBinding.spinnerAlignment.setSelection(ALIGNMENT_VALUE_ARRAY.indexOf(ALIGN_RIGHT))
+ viewBinding.spinnerWidth.apply {
+ setSelection(INDEX_DIMENSION_RATIO)
+ updateDimensionUi(this)
+ }
+ viewBinding.spinnerHeight.apply {
+ setSelection(INDEX_DIMENSION_RATIO)
+ updateDimensionUi(this)
+ }
+ }
+
+ private fun initializeSeekbar(seekBar: SeekBar) {
+ seekBar.progress = 50
+ updateRatioText(seekBar)
+ }
+
+ private fun updateOverlayBoundsText(overlayInfo: OverlayInfo) {
+ viewBinding.textViewOverlayBounds.text =
+ resources.getString(R.string.overlay_bounds_text) +
+ overlayInfo.currentOverlayAttributes?.bounds.toString()
+ }
+
+ override fun onClick(button: View) {
+ val overlayAttributes = buildOverlayAttributesFromUi()
+ val isCustomizationMode =
+ demoActivityEmbeddingController.overlayMode.get() == OVERLAY_MODE_CUSTOMIZATION.value
+ when (button.id) {
+ R.id.button_launch_overlay_container -> {
+ if (isCustomizationMode) {
+ // Also update controller's overlayAttributes because the launch bounds are
+ // determined by calculator, which returns the overlayAttributes from
+ // the controller directly.
+ demoActivityEmbeddingController.overlayAttributes = overlayAttributes
+ }
+ try {
+ startActivity(
+ Intent().apply {
+ setClassName(
+ "androidx.window.demo2",
+ "androidx.window.demo2.embedding.UntrustedEmbeddingActivity"
+ )
+ },
+ ActivityOptions.makeBasic()
+ .toBundle()
+ .setOverlayCreateParams(
+ this,
+ OverlayCreateParams.Builder()
+ .setTag(overlayTag)
+ .setOverlayAttributes(
+ if (isCustomizationMode) {
+ overlayAttributes
+ } else {
+ DEFAULT_OVERLAY_ATTRIBUTES
+ }
+ )
+ .build()
+ )
+ )
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show()
+ }
+ }
+ R.id.button_launch_overlay_activity_a ->
+ startActivity(
+ Intent(this, OverlayAssociatedActivityA::class.java).apply {
+ if (viewBinding.checkboxReorderToFront.isChecked) {
+ flags = FLAG_ACTIVITY_REORDER_TO_FRONT
+ }
+ }
+ )
+ R.id.button_launch_overlay_activity_b ->
+ startActivity(
+ Intent(this, OverlayAssociatedActivityB::class.java),
+ overlayActivityStack?.let {
+ if (viewBinding.checkboxLaunchToOverlay.isChecked) {
+ ActivityOptions.makeBasic()
+ .toBundle()
+ .setLaunchingActivityStack(
+ this,
+ it,
+ )
+ } else {
+ null
+ }
+ }
+ )
+ R.id.button_finish_this_activity -> finish()
+ R.id.button_update_overlay_layout -> {
+ if (isCustomizationMode) {
+ demoActivityEmbeddingController.overlayAttributes = overlayAttributes
+ ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
+ } else {
+ overlayController.updateOverlayAttributes(overlayTag, overlayAttributes)
+ }
+ }
+ }
+ }
+
+ private fun buildOverlayAttributesFromUi(): OverlayAttributes {
+ val spinnerPosition = viewBinding.spinnerAlignment
+ val spinnerWidth = viewBinding.spinnerWidth
+ val spinnerHeight = viewBinding.spinnerHeight
+
+ return OverlayAttributes.Builder()
+ .setBounds(
+ EmbeddingBounds(
+ ALIGNMENT_VALUE_ARRAY[spinnerPosition.selectedItemPosition],
+ createDimensionFromUi(spinnerWidth),
+ createDimensionFromUi(spinnerHeight),
+ )
+ )
+ .build()
+ }
+
+ private fun createDimensionFromUi(spinner: Spinner): Dimension =
+ when (val position = spinner.selectedItemPosition) {
+ INDEX_DIMENSION_EXPAND -> Dimension.DIMENSION_EXPANDED
+ INDEX_DIMENSION_HINGE -> Dimension.DIMENSION_HINGE
+ INDEX_DIMENSION_RATIO ->
+ Dimension.ratio(
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.seekBarWidthInRatio.progress.toFloat() / 100
+ } else {
+ viewBinding.seekBarHeightInRatio.progress.toFloat() / 100
+ }
+ )
+ INDEX_DIMENSION_PIXEL ->
+ Dimension.pixel(
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.editTextNumberDecimalWidthInPixel.text.toString().toInt()
+ } else {
+ viewBinding.editTextNumberDecimalHeightInPixel.text.toString().toInt()
+ }
+ )
+ else -> throw IllegalStateException("Unknown spinner index: $position")
+ }
+
+ override fun onCheckedChanged(group: RadioGroup, id: Int) {
+ demoActivityEmbeddingController.overlayMode.set(
+ when (id) {
+ R.id.radioButton_simple_overlay -> OVERLAY_MODE_SIMPLE.value
+ R.id.radioButton_change_with_orientation ->
+ OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
+ R.id.radioButton_customization -> OVERLAY_MODE_CUSTOMIZATION.value
+ else -> throw IllegalArgumentException("Unrecognized id $id")
+ }
+ )
+ }
+
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ if (
+ parent is Spinner &&
+ parent in arrayOf(viewBinding.spinnerWidth, viewBinding.spinnerHeight)
+ ) {
+ updateDimensionUi(parent)
+ }
+ }
+
+ private fun updateDimensionUi(spinner: Spinner) {
+ val textViewRatio =
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.textViewWidthInRatio
+ } else {
+ viewBinding.textViewHeightInRatio
+ }
+ val seekBarRatio =
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.seekBarWidthInRatio
+ } else {
+ viewBinding.seekBarHeightInRatio
+ }
+ val textViewPixel =
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.textViewWidthInPixel
+ } else {
+ viewBinding.textViewHeightInPixel
+ }
+ val editTextPixel =
+ if (spinner.isSpinnerWidth()) {
+ viewBinding.editTextNumberDecimalWidthInPixel
+ } else {
+ viewBinding.editTextNumberDecimalHeightInPixel
+ }
+ when (spinner.selectedItemPosition) {
+ INDEX_DIMENSION_EXPAND,
+ INDEX_DIMENSION_HINGE -> {
+ textViewRatio.visibility = View.GONE
+ seekBarRatio.visibility = View.GONE
+ textViewPixel.visibility = View.GONE
+ editTextPixel.visibility = View.GONE
+ }
+ INDEX_DIMENSION_RATIO -> {
+ textViewRatio.visibility = View.VISIBLE
+ seekBarRatio.visibility = View.VISIBLE
+ textViewPixel.visibility = View.GONE
+ editTextPixel.visibility = View.GONE
+ initializeSeekbar(seekBarRatio)
+ }
+ INDEX_DIMENSION_PIXEL -> {
+ textViewRatio.visibility = View.GONE
+ seekBarRatio.visibility = View.GONE
+ textViewPixel.visibility = View.VISIBLE
+ editTextPixel.visibility = View.VISIBLE
+ editTextPixel.text.clear()
+ }
+ }
+ }
+
+ private fun Spinner.isSpinnerWidth() = this == viewBinding.spinnerWidth
+
+ override fun onNothingSelected(view: AdapterView<*>?) {
+ // Auto-generated method stub
+ }
+
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ updateRatioText(seekBar)
+ }
+
+ private fun updateRatioText(seekBar: SeekBar) {
+ if (seekBar.isSeekBarWidthInRatio()) {
+ viewBinding.textViewWidthInRatio.text =
+ resources.getString(R.string.width_in_ratio) +
+ (seekBar.progress.toFloat() / 100).toString()
+ } else {
+ viewBinding.textViewHeightInRatio.text =
+ resources.getString(R.string.height_in_ratio) +
+ (seekBar.progress.toFloat() / 100).toString()
+ }
+ }
+
+ private fun SeekBar.isSeekBarWidthInRatio(): Boolean = this == viewBinding.seekBarWidthInRatio
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {
+ // Auto-generated method stub
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ // Auto-generated method stub
+ }
+
+ @JvmInline
+ internal value class OverlayMode(val value: Int) {
+ companion object {
+ val OVERLAY_MODE_SIMPLE = OverlayMode(0)
+ val OVERLAY_MODE_CHANGE_WITH_ORIENTATION = OverlayMode(1)
+ val OVERLAY_MODE_CUSTOMIZATION = OverlayMode(2)
+ }
+ }
+
+ companion object {
+ internal const val OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION = 8
+
+ internal val DEFAULT_OVERLAY_ATTRIBUTES =
+ OverlayAttributes(
+ EmbeddingBounds(
+ ALIGN_RIGHT,
+ Dimension.ratio(0.5f),
+ Dimension.ratio(0.8f),
+ )
+ )
+
+ private val POSITION_TEXT_ARRAY = arrayOf("top", "left", "bottom", "right")
+ private val ALIGNMENT_VALUE_ARRAY =
+ arrayListOf(
+ ALIGN_TOP,
+ ALIGN_LEFT,
+ ALIGN_BOTTOM,
+ ALIGN_RIGHT,
+ )
+
+ private val DIMENSION_TYPE_TEXT_ARRAY =
+ arrayOf(
+ "expand to the task",
+ "follow the hinge",
+ "dimension in ratio",
+ "dimension in pixel",
+ )
+ private const val INDEX_DIMENSION_EXPAND = 0
+ private const val INDEX_DIMENSION_HINGE = 1
+ private const val INDEX_DIMENSION_RATIO = 2
+ private const val INDEX_DIMENSION_PIXEL = 3
+ }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
new file mode 100644
index 0000000..0e35bb5
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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.demo.embedding
+
+class OverlayAssociatedActivityA : OverlayActivityBase()
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
new file mode 100644
index 0000000..edc9b7e
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 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.demo.embedding
+
+import android.graphics.Color
+import android.os.Bundle
+
+class OverlayAssociatedActivityB : OverlayActivityBase() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ viewBinding.rootOverlayActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
+ }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
index aec91b9..4520b2c 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
@@ -18,6 +18,7 @@
import android.content.Intent
import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.window.demo.R
@@ -25,8 +26,9 @@
open class SplitActivityB : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#fff3e0"))
+ val color = Color.parseColor("#fff3e0")
+ findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+ window.setBackgroundDrawable(ColorDrawable(color))
if (intent.getBooleanExtra(EXTRA_LAUNCH_C_TO_SIDE, false)) {
startActivity(Intent(this, SplitActivityC::class.java))
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
index 7ff6884..de4bcef 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
@@ -18,12 +18,15 @@
import static android.app.PendingIntent.FLAG_IMMUTABLE;
-import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_AVAILABLE;
+import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED;
+import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_UNAVAILABLE;
import static androidx.window.embedding.SplitRule.FinishBehavior.ADJACENT;
import static androidx.window.embedding.SplitRule.FinishBehavior.ALWAYS;
import static androidx.window.embedding.SplitRule.FinishBehavior.NEVER;
import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -36,14 +39,21 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
import androidx.core.util.Consumer;
import androidx.window.WindowSdkExtensions;
import androidx.window.demo.R;
+import androidx.window.demo.common.EdgeToEdgeActivity;
import androidx.window.demo.databinding.ActivitySplitActivityLayoutBinding;
import androidx.window.embedding.ActivityEmbeddingController;
+import androidx.window.embedding.ActivityEmbeddingOptions;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
+import androidx.window.embedding.DividerAttributes;
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes;
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes;
+import androidx.window.embedding.EmbeddedActivityWindowInfo;
+import androidx.window.embedding.EmbeddingAnimationParams;
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec;
import androidx.window.embedding.EmbeddingRule;
import androidx.window.embedding.RuleController;
import androidx.window.embedding.SplitAttributes;
@@ -51,7 +61,9 @@
import androidx.window.embedding.SplitInfo;
import androidx.window.embedding.SplitPairFilter;
import androidx.window.embedding.SplitPairRule;
+import androidx.window.embedding.SplitPinRule;
import androidx.window.embedding.SplitPlaceholderRule;
+import androidx.window.java.embedding.ActivityEmbeddingControllerCallbackAdapter;
import androidx.window.java.embedding.SplitControllerCallbackAdapter;
import java.util.HashSet;
@@ -62,7 +74,7 @@
* Sample showcase of split activity rules. Allows the user to select some split configuration
* options with checkboxes and launch activities with those options applied.
*/
-public class SplitActivityBase extends AppCompatActivity
+public class SplitActivityBase extends EdgeToEdgeActivity
implements CompoundButton.OnCheckedChangeListener {
private static final String TAG = "SplitActivityTest";
@@ -76,7 +88,11 @@
*/
private SplitControllerCallbackAdapter mSplitControllerAdapter;
private RuleController mRuleController;
- private SplitInfoCallback mCallback;
+ private SplitInfoCallback mSplitInfoCallback;
+
+ private ActivityEmbeddingController mActivityEmbeddingController;
+ private ActivityEmbeddingControllerCallbackAdapter mActivityEmbeddingControllerCallbackAdapter;
+ private EmbeddedActivityWindowInfoCallback mEmbeddedActivityWindowInfoCallbackCallback;
private ActivitySplitActivityLayoutBinding mViewBinding;
@@ -86,9 +102,28 @@
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ final SplitController splitController = SplitController.getInstance(this);
+ final SplitController.SplitSupportStatus splitSupportStatus =
+ splitController.getSplitSupportStatus();
+ if (splitSupportStatus == SPLIT_UNAVAILABLE) {
+ Toast.makeText(this, R.string.toast_split_not_available,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ } else if (splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
+ Toast.makeText(this, R.string.toast_split_not_support,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
mViewBinding = ActivitySplitActivityLayoutBinding.inflate(getLayoutInflater());
setContentView(mViewBinding.getRoot());
+ final int extensionVersion = WindowSdkExtensions.getInstance().getExtensionVersion();
+ mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this);
+
// Setup activity launch buttons and config options.
mViewBinding.launchB.setOnClickListener((View v) ->
startActivity(new Intent(this, SplitActivityB.class)));
@@ -99,10 +134,19 @@
});
mViewBinding.launchE.setOnClickListener((View v) -> {
Bundle bundle = null;
+ if (mViewBinding.setLaunchingEInActivityStack.isChecked()) {
+ try {
+ bundle = ActivityEmbeddingOptions.setLaunchingActivityStack(
+ ActivityOptions.makeBasic().toBundle(), this,
+ mActivityEmbeddingController.getActivityStack(this));
+ } catch (UnsupportedOperationException ex) {
+ Log.w(TAG, "#setLaunchingActivityStack is not supported", ex);
+ }
+ }
startActivity(new Intent(this, SplitActivityE.class), bundle);
});
- if (WindowSdkExtensions.getInstance().getExtensionVersion() < 3) {
- mViewBinding.setLaunchingEInActivityStack.setEnabled(false);
+ if (extensionVersion < 3) {
+ mViewBinding.setLaunchingEInActivityStack.setVisibility(View.GONE);
}
mViewBinding.launchF.setOnClickListener((View v) ->
startActivity(new Intent(this, SplitActivityF.class)));
@@ -158,6 +202,44 @@
});
mViewBinding.launchExpandedDialogButton.setOnClickListener((View v) ->
startActivity(new Intent(this, ExpandedDialogActivity.class)));
+ mViewBinding.launchDialogActivityButton.setOnClickListener((View v) ->
+ startActivity(new Intent(this, DialogActivity.class)));
+ mViewBinding.launchDialogButton.setOnClickListener((View v) ->
+ new AlertDialog.Builder(this)
+ .setTitle("Alert dialog demo")
+ .setMessage("This is a dialog demo").create().show());
+
+ if (extensionVersion < 5) {
+ mViewBinding.pinTopActivityStackButton.setVisibility(View.GONE);
+ mViewBinding.unpinTopActivityStackButton.setVisibility(View.GONE);
+ } else {
+ mViewBinding.pinTopActivityStackButton.setOnClickListener((View v) -> {
+ splitController.pinTopActivityStack(getTaskId(),
+ new SplitPinRule.Builder().setSticky(
+ mViewBinding.stickyPinRule.isChecked()).build());
+ }
+ );
+ mViewBinding.unpinTopActivityStackButton.setOnClickListener((View v) -> {
+ splitController.unpinTopActivityStack(getTaskId());
+ }
+ );
+ }
+ if (extensionVersion < 6) {
+ mViewBinding.dividerCheckBox.setVisibility(View.GONE);
+ mViewBinding.draggableDividerCheckBox.setVisibility(View.GONE);
+ } else {
+ mViewBinding.dividerCheckBox.setOnCheckedChangeListener(this);
+ mViewBinding.draggableDividerCheckBox.setOnCheckedChangeListener(this);
+ }
+ if (extensionVersion < 7) {
+ mViewBinding.openAnimationJumpCutCheckBox.setVisibility(View.GONE);
+ mViewBinding.closeAnimationJumpCutCheckBox.setVisibility(View.GONE);
+ mViewBinding.changeAnimationJumpCutCheckBox.setVisibility(View.GONE);
+ } else {
+ mViewBinding.openAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+ mViewBinding.closeAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+ mViewBinding.changeAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+ }
// Listen for split configuration checkboxes to update the rules before launching
// activities.
@@ -169,42 +251,84 @@
mViewBinding.fullscreenECheckBox.setOnCheckedChangeListener(this);
mViewBinding.splitWithFCheckBox.setOnCheckedChangeListener(this);
- final SplitController splitController = SplitController.getInstance(this);
+ if (extensionVersion < 6) {
+ mViewBinding.buttonLaunchOverlayAssociatedActivity.setVisibility(View.GONE);
+ } else {
+ mViewBinding.buttonLaunchOverlayAssociatedActivity.setOnClickListener((View v) ->
+ startActivity(new Intent(this, OverlayAssociatedActivityA.class)));
+ }
+
mSplitControllerAdapter = new SplitControllerCallbackAdapter(splitController);
- if (splitController.getSplitSupportStatus() != SPLIT_AVAILABLE) {
- Toast.makeText(this, R.string.toast_split_not_support,
- Toast.LENGTH_SHORT).show();
- finish();
- return;
+ if (extensionVersion >= 6) {
+ mActivityEmbeddingControllerCallbackAdapter =
+ new ActivityEmbeddingControllerCallbackAdapter(mActivityEmbeddingController);
+
+ // The EmbeddedActivityWindowInfoListener will only be triggered when the activity is
+ // embedded and visible (just like Activity#onConfigurationChanged).
+ // Register it in #onCreate instead of #onStart so that when the embedded status is
+ // changed to non-embedded before #onStart (like screen rotation when this activity is
+ // in background), the listener will be triggered right after #onStart.
+ // Otherwise, if registered in #onStart, it will not be triggered on registration
+ // because the activity is not embedded, which results it shows the stale info.
+ mEmbeddedActivityWindowInfoCallbackCallback = new EmbeddedActivityWindowInfoCallback();
+ mActivityEmbeddingControllerCallbackAdapter.addEmbeddedActivityWindowInfoListener(
+ this, Runnable::run, mEmbeddedActivityWindowInfoCallbackCallback);
}
mRuleController = RuleController.getInstance(this);
}
@Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mActivityEmbeddingControllerCallbackAdapter != null) {
+ mActivityEmbeddingControllerCallbackAdapter.removeEmbeddedActivityWindowInfoListener(
+ mEmbeddedActivityWindowInfoCallbackCallback);
+ mEmbeddedActivityWindowInfoCallbackCallback = null;
+ }
+ }
+
+ @Override
protected void onStart() {
super.onStart();
- mCallback = new SplitInfoCallback();
- mSplitControllerAdapter.addSplitListener(this, Runnable::run, mCallback);
+ mSplitInfoCallback = new SplitInfoCallback();
+ mSplitControllerAdapter.addSplitListener(this, Runnable::run, mSplitInfoCallback);
}
@Override
protected void onStop() {
super.onStop();
- mSplitControllerAdapter.removeSplitListener(mCallback);
- mCallback = null;
+ mSplitControllerAdapter.removeSplitListener(mSplitInfoCallback);
+ mSplitInfoCallback = null;
}
/** Updates the embedding status when receives callback from the extension. */
- class SplitInfoCallback implements Consumer<List<SplitInfo>> {
+ private class SplitInfoCallback implements Consumer<List<SplitInfo>> {
@Override
public void accept(List<SplitInfo> splitInfoList) {
runOnUiThread(() -> {
- updateEmbeddedStatus();
+ if (mActivityEmbeddingControllerCallbackAdapter == null) {
+ // Otherwise, the embedded status will be updated from
+ // EmbeddedActivityWindowInfoCallback.
+ updateEmbeddedStatus(mActivityEmbeddingController.isActivityEmbedded(
+ SplitActivityBase.this));
+ }
updateCheckboxesFromCurrentConfig();
});
}
}
+ /** Updates the embedding status when receives callback from the extension. */
+ private class EmbeddedActivityWindowInfoCallback implements
+ Consumer<EmbeddedActivityWindowInfo> {
+ @Override
+ public void accept(EmbeddedActivityWindowInfo embeddedActivityWindowInfo) {
+ runOnUiThread(() -> {
+ updateEmbeddedStatus(embeddedActivityWindowInfo.isEmbedded());
+ updateEmbeddedWindowInfo(embeddedActivityWindowInfo);
+ });
+ }
+ }
+
/** Called on checkbox changed. */
@Override
public void onCheckedChanged(@NonNull CompoundButton c, boolean isChecked) {
@@ -328,8 +452,37 @@
/** Updates the split rules based on the current selection on checkboxes. */
private void updateRulesFromCheckboxes() {
mRuleController.clearRules();
+
+ final DividerAttributes dividerAttributes;
+ if (mViewBinding.dividerCheckBox.isChecked()) {
+ if (mViewBinding.draggableDividerCheckBox.isChecked()) {
+ dividerAttributes = new DraggableDividerAttributes.Builder()
+ .setWidthDp(1)
+ .setDraggingToFullscreenAllowed(true)
+ .build();
+ } else {
+ dividerAttributes = new FixedDividerAttributes.Builder().setWidthDp(1).build();
+ }
+ } else {
+ dividerAttributes = DividerAttributes.NO_DIVIDER;
+ }
+ final EmbeddingAnimationParams.Builder animationParamsBuilder =
+ new EmbeddingAnimationParams.Builder();
+ if (mViewBinding.openAnimationJumpCutCheckBox.isChecked()) {
+ animationParamsBuilder.setOpenAnimation(AnimationSpec.JUMP_CUT);
+ }
+ if (mViewBinding.closeAnimationJumpCutCheckBox.isChecked()) {
+ animationParamsBuilder.setCloseAnimation(AnimationSpec.JUMP_CUT);
+ }
+ if (mViewBinding.changeAnimationJumpCutCheckBox.isChecked()) {
+ animationParamsBuilder.setChangeAnimation(AnimationSpec.JUMP_CUT);
+ }
+ final EmbeddingAnimationParams animationParams = animationParamsBuilder.build();
+
final SplitAttributes defaultSplitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(SPLIT_RATIO))
+ .setDividerAttributes(dividerAttributes)
+ .setAnimationParams(animationParams)
.build();
if (mViewBinding.splitMainCheckBox.isChecked()) {
@@ -349,6 +502,14 @@
mRuleController.addRule(rule);
}
+ mViewBinding.draggableDividerCheckBox.setEnabled(mViewBinding.dividerCheckBox.isChecked());
+ mViewBinding.openAnimationJumpCutCheckBox.setEnabled(
+ mViewBinding.splitMainCheckBox.isChecked());
+ mViewBinding.closeAnimationJumpCutCheckBox.setEnabled(
+ mViewBinding.splitMainCheckBox.isChecked());
+ mViewBinding.changeAnimationJumpCutCheckBox.setEnabled(
+ mViewBinding.splitMainCheckBox.isChecked());
+
if (mViewBinding.usePlaceholderCheckBox.isChecked()) {
// Split B with placeholder.
final Set<ActivityFilter> activityFilters = new HashSet<>();
@@ -436,11 +597,21 @@
}
/** Updates the status label that says when an activity is embedded. */
- void updateEmbeddedStatus() {
- if (ActivityEmbeddingController.getInstance(this).isActivityEmbedded(this)) {
- mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.VISIBLE);
- } else {
- mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.GONE);
+ private void updateEmbeddedStatus(boolean isEmbedded) {
+ mViewBinding.activityEmbeddedStatusTextView.setVisibility(isEmbedded
+ ? View.VISIBLE
+ : View.GONE);
+ }
+
+ private void updateEmbeddedWindowInfo(
+ @NonNull EmbeddedActivityWindowInfo info) {
+ Log.d(TAG, "EmbeddedActivityWindowInfo changed for r=" + this + "\ninfo=" + info);
+ if (!info.isEmbedded()) {
+ mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.GONE);
+ return;
}
+ mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.VISIBLE);
+ mViewBinding.activityEmbeddedBoundsTextView.setText(
+ "Embedded bounds=" + info.getBoundsInParentHost());
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
index dcbbb3b..7b61240c 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
@@ -17,6 +17,7 @@
package androidx.window.demo.embedding
import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.window.demo.R
@@ -24,7 +25,8 @@
open class SplitActivityC : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#e8f5e9"))
+ val color = Color.parseColor("#e8f5e9")
+ findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+ window.setBackgroundDrawable(ColorDrawable(color))
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
index 705dfee..000ff2b 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
@@ -17,6 +17,7 @@
package androidx.window.demo.embedding
import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.window.demo.R
@@ -24,7 +25,8 @@
open class SplitActivityD : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#eeeeee"))
+ val color = Color.parseColor("#eeeeee")
+ findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+ window.setBackgroundDrawable(ColorDrawable(color))
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
index f4f6bff..7e79b83 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
@@ -19,27 +19,34 @@
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
-import android.view.View
import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
-import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivitySplitActivityListDetailLayoutBinding
-open class SplitActivityDetail : AppCompatActivity() {
+open class SplitActivityDetail : EdgeToEdgeActivity() {
+
+ private lateinit var viewBinding: ActivitySplitActivityListDetailLayoutBinding
+ private lateinit var itemDetailTextView: TextView
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_split_activity_list_detail_layout)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#fff3e0"))
- findViewById<TextView>(R.id.item_detail_text)
- .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
+ viewBinding = ActivitySplitActivityListDetailLayoutBinding.inflate(layoutInflater)
+ viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#fff3e0"))
+ setContentView(viewBinding.root)
+ itemDetailTextView = viewBinding.itemDetailText
+
+ itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
+
+ window.decorView.setOnFocusChangeListener { _, focus ->
+ itemDetailTextView.text = "${itemDetailTextView.text} focus=$focus"
+ }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- findViewById<TextView>(R.id.item_detail_text)
- .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
+ itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
}
companion object {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
index 2766a7c..a530d22 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
@@ -17,6 +17,7 @@
package androidx.window.demo.embedding
import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.window.demo.R
@@ -24,7 +25,8 @@
open class SplitActivityE : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#ede7f6"))
+ val color = Color.parseColor("#ede7f6")
+ findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+ window.setBackgroundDrawable(ColorDrawable(color))
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
index 5fbf9ce..d8161d0 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
@@ -17,6 +17,7 @@
package androidx.window.demo.embedding
import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.window.demo.R
@@ -24,7 +25,8 @@
open class SplitActivityF : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#ffebee"))
+ val color = Color.parseColor("#ffebee")
+ findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+ window.setBackgroundDrawable(ColorDrawable(color))
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
index b7920d8..d7e1cf4 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
@@ -21,23 +21,25 @@
import android.os.Bundle
import android.view.View
import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivitySplitActivityListLayoutBinding
import androidx.window.demo.embedding.SplitActivityDetail.Companion.EXTRA_SELECTED_ITEM
import androidx.window.embedding.SplitController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-open class SplitActivityList : AppCompatActivity() {
+private lateinit var viewBinding: ActivitySplitActivityListLayoutBinding
+
+open class SplitActivityList : EdgeToEdgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_split_activity_list_layout)
- findViewById<View>(R.id.root_split_activity_layout)
- .setBackgroundColor(Color.parseColor("#e0f7fa"))
+ viewBinding = ActivitySplitActivityListLayoutBinding.inflate(layoutInflater)
+ setContentView(viewBinding.root)
+ viewBinding.root.setBackgroundColor(Color.parseColor("#e0f7fa"))
val splitController = SplitController.getInstance(this)
lifecycleScope.launch {
@@ -47,7 +49,7 @@
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
splitController.splitInfoList(this@SplitActivityList).collect { newSplitInfos ->
withContext(Dispatchers.Main) {
- findViewById<View>(R.id.infoButton).visibility =
+ viewBinding.infoButton.visibility =
if (newSplitInfos.isEmpty()) View.VISIBLE else View.GONE
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
index 687f931..921f72d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
@@ -18,10 +18,10 @@
import android.graphics.Color
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.databinding.ActivitySplitActivityPlaceholderLayoutBinding
-open class SplitActivityPlaceholder : AppCompatActivity() {
+open class SplitActivityPlaceholder : EdgeToEdgeActivity() {
lateinit var viewBinding: ActivitySplitActivityPlaceholderLayoutBinding
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
index 6badb3e..a2fa58be 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
@@ -18,11 +18,11 @@
import android.os.Bundle
import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
@@ -35,7 +35,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-open class SplitAttributesToggleActivityBase : AppCompatActivity() {
+open class SplitAttributesToggleActivityBase : EdgeToEdgeActivity() {
internal lateinit var splitController: SplitController
internal lateinit var ruleController: RuleController
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
index ae0246a..1ef955d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
@@ -80,6 +80,7 @@
splitRuleFoldingAwareAttrsRadioButton.isEnabled = false
viewBinding.splitRuleUseCustomizedSplitAttributes.isEnabled = false
}
+
viewBinding.startPrimaryActivityButton.setOnClickListener(this)
viewBinding.useStickyPlaceholderCheckBox.setOnCheckedChangeListener(this)
viewBinding.usePlaceholderCheckBox.setOnCheckedChangeListener(this)
@@ -238,6 +239,13 @@
if (apiLevel < 3) {
append("Finishing secondary activities is not supported on this device!\n")
}
+ if (
+ viewBinding.finishSecondaryActivitiesButton.isEnabled &&
+ getSplitRule<SplitPlaceholderRule>() != null
+ ) {
+ append(resources.getString(R.string.show_placeholder_warning))
+ append("\n")
+ }
}
withContext(Dispatchers.Main) { viewBinding.warningMessageTextView.text = warningMessages }
}
@@ -429,8 +437,20 @@
R.id.split_rule_layout_direction_spinner ->
demoActivityEmbeddingController.customizedLayoutDirection =
CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
+ R.id.animation_background_dropdown ->
+ demoActivityEmbeddingController.animationBackground =
+ DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
+ R.id.open_animation_dropdown ->
+ demoActivityEmbeddingController.openAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+ R.id.close_animation_dropdown ->
+ demoActivityEmbeddingController.closeAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+ R.id.change_animation_dropdown ->
+ demoActivityEmbeddingController.changeAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
}
- splitController.invalidateTopVisibleSplitAttributes()
+ activityEmbeddingController.invalidateVisibleActivityStacks()
}
override fun onNothingSelected(view: AdapterView<*>?) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
index 0e684d2..38587a7 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
@@ -21,14 +21,19 @@
import android.graphics.Color
import android.os.Bundle
import android.view.View
+import android.widget.ArrayAdapter
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.R
import androidx.window.embedding.ActivityStack
import androidx.window.embedding.SplitInfo
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+@OptIn(ExperimentalWindowApi::class)
class SplitAttributesTogglePrimaryActivity :
SplitAttributesToggleMainActivity(), View.OnClickListener {
@@ -40,6 +45,8 @@
viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
+ val isRuntimeApiSupported = WindowSdkExtensions.getInstance().extensionVersion >= 3
+
secondaryActivityIntent = Intent(this, SplitAttributesToggleSecondaryActivity::class.java)
if (intent.getBooleanExtra(EXTRA_LAUNCH_SECONDARY, false)) {
@@ -51,6 +58,69 @@
// Enable to finish secondary ActivityStacks for primary Activity.
viewBinding.finishSecondaryActivitiesDivider.visibility = View.VISIBLE
+ val finishSecondaryActivitiesButton =
+ viewBinding.finishSecondaryActivitiesButton.apply {
+ visibility = View.VISIBLE
+ if (!isRuntimeApiSupported) {
+ isEnabled = false
+ } else {
+ setOnClickListener(this@SplitAttributesTogglePrimaryActivity)
+ }
+ }
+
+ // Animation background
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
+ val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
+ animationBackgroundDropdown.visibility = View.VISIBLE
+ viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
+ viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
+ animationBackgroundDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
+ )
+ animationBackgroundDropdown.onItemSelectedListener = this
+ }
+
+ // Animation transitions
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 7) {
+ val openAnimationDropdown = viewBinding.openAnimationDropdown
+ openAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.openAnimationDivider.visibility = View.VISIBLE
+ viewBinding.openAnimationTextView.visibility = View.VISIBLE
+ openAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ openAnimationDropdown.onItemSelectedListener = this
+
+ val closeAnimationDropdown = viewBinding.closeAnimationDropdown
+ closeAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.closeAnimationDivider.visibility = View.VISIBLE
+ viewBinding.closeAnimationTextView.visibility = View.VISIBLE
+ closeAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ closeAnimationDropdown.onItemSelectedListener = this
+
+ val changeAnimationDropdown = viewBinding.changeAnimationDropdown
+ changeAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.changeAnimationDivider.visibility = View.VISIBLE
+ viewBinding.changeAnimationTextView.visibility = View.VISIBLE
+ changeAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ changeAnimationDropdown.onItemSelectedListener = this
+ }
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
@@ -61,6 +131,7 @@
.splitInfoList(this@SplitAttributesTogglePrimaryActivity)
.onEach { updateUiFromRules() }
.collect { splitInfoList ->
+ finishSecondaryActivitiesButton.isEnabled = splitInfoList.isNotEmpty()
activityStacks =
splitInfoList.mapTo(mutableSetOf()) { splitInfo ->
splitInfo.getTheOtherActivityStack(
@@ -78,4 +149,14 @@
} else {
primaryActivityStack
}
+
+ override fun onClick(button: View) {
+ super.onClick(button)
+ when (button.id) {
+ R.id.finish_secondary_activities_button -> {
+ applyRules()
+ activityEmbeddingController.finishActivityStacks(activityStacks)
+ }
+ }
+ }
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
index 129d0aa9..21cc4a7 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
@@ -175,7 +175,7 @@
val enableFullscreenMode =
DemoActivityEmbeddingController.getInstance().shouldExpandSecondaryContainer
enableFullscreenMode.set(!enableFullscreenMode.get())
- splitController.invalidateTopVisibleSplitAttributes()
+ activityEmbeddingController.invalidateVisibleActivityStacks()
} else {
// Update the top splitInfo if single default split Attributes is used.
splitController.updateSplitAttributes(
@@ -201,7 +201,7 @@
demoActivityEmbeddingController.customizedLayoutDirection =
CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
}
- splitController.invalidateTopVisibleSplitAttributes()
+ activityEmbeddingController.invalidateVisibleActivityStacks()
}
override fun onNothingSelected(view: AdapterView<*>?) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
index a6c23d3..ff94d18 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
@@ -21,23 +21,26 @@
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
+import android.widget.ArrayAdapter
import android.widget.CompoundButton
import android.widget.RadioGroup
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.WindowSdkExtensions
import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.databinding.ActivitySplitDeviceStateLayoutBinding
+import androidx.window.embedding.EmbeddingAnimationParams
import androidx.window.embedding.EmbeddingRule
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitController
-import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_ERROR_PROPERTY_NOT_DECLARED
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
import androidx.window.embedding.SplitInfo
import androidx.window.embedding.SplitPairFilter
import androidx.window.embedding.SplitPairRule
@@ -46,7 +49,7 @@
import kotlinx.coroutines.withContext
open class SplitDeviceStateActivityBase :
- AppCompatActivity(),
+ EdgeToEdgeActivity(),
View.OnClickListener,
RadioGroup.OnCheckedChangeListener,
CompoundButton.OnCheckedChangeListener,
@@ -64,6 +67,8 @@
private lateinit var activityA: ComponentName
private lateinit var activityB: ComponentName
+ private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
/** The last selected split rule id. */
private var lastCheckedRuleId = 0
@@ -73,7 +78,11 @@
super.onCreate(savedInstanceState)
viewBinding = ActivitySplitDeviceStateLayoutBinding.inflate(layoutInflater)
splitController = SplitController.getInstance(this)
- if (splitController.splitSupportStatus != SPLIT_AVAILABLE) {
+ if (splitController.splitSupportStatus == SPLIT_UNAVAILABLE) {
+ Toast.makeText(this, R.string.toast_split_not_available, Toast.LENGTH_SHORT).show()
+ finish()
+ return
+ } else if (splitController.splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
Toast.makeText(this, R.string.toast_split_not_support, Toast.LENGTH_SHORT).show()
finish()
return
@@ -118,6 +127,62 @@
resources.getString(R.string.split_attributes_calculator_not_supported)
}
+ // Animation background
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 5 && componentName == activityA) {
+ // Show on only the primary activity.
+ val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
+ animationBackgroundDropdown.visibility = View.VISIBLE
+ viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
+ viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
+ animationBackgroundDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
+ )
+ animationBackgroundDropdown.onItemSelectedListener = this
+ }
+
+ // Animation transitions
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 7 && componentName == activityA) {
+ // Show on only the primary activity.
+ val openAnimationDropdown = viewBinding.openAnimationDropdown
+ openAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.openAnimationDivider.visibility = View.VISIBLE
+ viewBinding.openAnimationTextView.visibility = View.VISIBLE
+ openAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ openAnimationDropdown.onItemSelectedListener = this
+
+ val closeAnimationDropdown = viewBinding.closeAnimationDropdown
+ closeAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.closeAnimationDivider.visibility = View.VISIBLE
+ viewBinding.closeAnimationTextView.visibility = View.VISIBLE
+ closeAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ closeAnimationDropdown.onItemSelectedListener = this
+
+ val changeAnimationDropdown = viewBinding.changeAnimationDropdown
+ changeAnimationDropdown.visibility = View.VISIBLE
+ viewBinding.changeAnimationDivider.visibility = View.VISIBLE
+ viewBinding.changeAnimationTextView.visibility = View.VISIBLE
+ changeAnimationDropdown.adapter =
+ ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+ )
+ changeAnimationDropdown.onItemSelectedListener = this
+ }
+
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
@@ -171,6 +236,20 @@
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ when (parent?.id) {
+ R.id.animation_background_dropdown ->
+ demoActivityEmbeddingController.animationBackground =
+ DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
+ R.id.open_animation_dropdown ->
+ demoActivityEmbeddingController.openAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+ R.id.close_animation_dropdown ->
+ demoActivityEmbeddingController.closeAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+ R.id.change_animation_dropdown ->
+ demoActivityEmbeddingController.changeAnimation =
+ DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+ }
updateSplitPairRuleWithRadioButtonId(lastCheckedRuleId)
}
@@ -246,6 +325,14 @@
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+ .setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+ .setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+ .setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+ .build()
+ )
.build()
// Use the tag to control the rule how to change split attributes with the current state
var tag =
@@ -292,7 +379,17 @@
private suspend fun updateSplitAttributesText(newSplitInfos: List<SplitInfo>) {
var splitAttributes: SplitAttributes =
- SplitAttributes.Builder().setSplitType(SPLIT_TYPE_EXPAND).build()
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_EXPAND)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+ .setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+ .setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+ .setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+ .build()
+ )
+ .build()
var suggestToFinishItself = false
// Traverse SplitInfos from the end because last SplitInfo has the highest z-order.
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
index aa3bdbc..d167778 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
@@ -17,14 +17,14 @@
package androidx.window.demo.embedding
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.databinding.ActivitySplitImeLayoutBinding
/**
* Sample showcase of split activity with input field. Allows the user to use IME with split
* activities. The split rule is defined in `main_split_config.xml`.
*/
-abstract class SplitImeActivityBase : AppCompatActivity() {
+abstract class SplitImeActivityBase : EdgeToEdgeActivity() {
lateinit var viewBinding: ActivitySplitImeLayoutBinding
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
index ae74e8f..a626a67 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
@@ -24,11 +24,11 @@
import android.widget.CompoundButton
import android.widget.RadioGroup
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
import androidx.window.demo.common.util.PictureInPictureUtil
import androidx.window.demo.databinding.ActivitySplitPipActivityLayoutBinding
import androidx.window.embedding.ActivityFilter
@@ -53,7 +53,7 @@
* applied.
*/
abstract class SplitPipActivityBase :
- AppCompatActivity(),
+ EdgeToEdgeActivity(),
CompoundButton.OnCheckedChangeListener,
View.OnClickListener,
RadioGroup.OnCheckedChangeListener {
diff --git a/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml
deleted file mode 100644
index 449fe9a..0000000
--- a/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2023 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.
- -->
-
-<ScrollView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/root_split_activity_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="10dp">
-
- <!-- Update to the latest configuration. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/latest_update_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/latest_configuration_title"
- app:hidePrevConfig="true"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="10dp"
- android:layout_marginBottom="10dp"
- android:background="#AAAAAA" />
-
- <!-- Update from Application DisplayListener. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/application_display_listener_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/application_display_listener_title"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="10dp"
- android:layout_marginBottom="10dp"
- android:background="#AAAAAA" />
-
- <!-- Update from Activity DisplayListener. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/activity_display_listener_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/activity_display_listener_title"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="10dp"
- android:layout_marginBottom="10dp"
- android:background="#AAAAAA" />
-
- <!-- Update from Application#onConfigurationChanged. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/application_configuration_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/application_configuration_title"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="10dp"
- android:layout_marginBottom="10dp"
- android:background="#AAAAAA" />
-
- <!-- Update from Activity#onConfigurationChanged. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/activity_configuration_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/activity_configuration_title"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="10dp"
- android:layout_marginBottom="10dp"
- android:background="#AAAAAA" />
-
- <!-- Update from WindowInfoTracker. -->
- <androidx.window.demo.coresdk.WindowStateView
- android:id="@+id/display_feature_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/display_feature_title"/>
-
- </LinearLayout>
-</ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
new file mode 100644
index 0000000..8650bcb
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 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.
+ -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root_overlay_activity_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/textView_overlay_bounds"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/overlay_bounds_text" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginTop="10dp"
+ android:background="#AAAAAA" />
+
+ <TextView
+ android:id="@+id/textView_choose_overlay_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Choose overlay layout:" />
+
+ <RadioGroup
+ android:id="@+id/radioGroup_choose_overlay_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <RadioButton
+ android:id="@+id/radioButton_simple_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch the overlay container to the right" />
+
+ <RadioButton
+ android:id="@+id/radioButton_change_with_orientation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Change overlay layout with orientation" />
+
+ <RadioButton
+ android:id="@+id/radioButton_customization"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Use customized overlay layout" />
+ </RadioGroup>
+
+ <TextView
+ android:id="@+id/textView_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Layout"
+ android:visibility="visible" />
+
+ <!-- UI to control position -->
+
+ <TextView
+ android:id="@+id/textView_position"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Position"
+ android:visibility="visible" />
+ <Spinner
+ android:id="@+id/spinner_alignment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ tools:visibility="visible" />
+
+ <!-- UI to control width -->
+
+ <TextView
+ android:id="@+id/textView_width"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Width"
+ android:visibility="visible" />
+ <Spinner
+ android:id="@+id/spinner_width"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ tools:visibility="visible" />
+ <TextView
+ android:id="@+id/textView_width_in_pixel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Width in pixel"
+ android:visibility="gone" />
+ <EditText
+ android:id="@+id/editTextNumberDecimal_width_in_pixel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="4"
+ android:inputType="numberDecimal"
+ android:visibility="gone" />
+ <TextView
+ android:id="@+id/textView_width_in_ratio"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/width_in_ratio" />
+ <SeekBar
+ android:id="@+id/seekBar_width_in_ratio"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:min="1"
+ android:max="99"
+ android:progress="50" />
+
+ <!-- UI to control height -->
+
+ <TextView
+ android:id="@+id/textView_height"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Height"
+ android:visibility="visible" />
+ <Spinner
+ android:id="@+id/spinner_height"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ tools:visibility="visible" />
+ <TextView
+ android:id="@+id/textView_height_in_pixel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Height in pixel"
+ android:visibility="gone" />
+ <EditText
+ android:id="@+id/editTextNumberDecimal_height_in_pixel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="4"
+ android:inputType="numberDecimal"
+ android:visibility="gone" />
+ <TextView
+ android:id="@+id/textView_height_in_ratio"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/height_in_ratio"
+ android:visibility="gone" />
+ <SeekBar
+ android:id="@+id/seekBar_height_in_ratio"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:min="1"
+ android:max="99"
+ android:progress="80"
+ android:visibility="gone" />
+
+ <!-- UI to update overlay layout -->
+
+ <Button
+ android:id="@+id/button_update_overlay_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Update Overlay layout" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginTop="10dp"
+ android:background="#AAAAAA" />
+
+ <Button
+ android:id="@+id/button_launch_overlay_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch the overlay container" />
+
+ <CheckBox
+ android:id="@+id/checkbox_reorder_to_front"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Reorder Activity A to the front" />
+
+ <Button
+ android:id="@+id/button_launch_overlay_activity_a"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Overlay Associated Activity A" />
+
+ <CheckBox
+ android:id="@+id/checkbox_launch_to_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Activity B to the overlay container" />
+
+ <Button
+ android:id="@+id/button_launch_overlay_activity_b"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Overlay Associated Activity B" />
+
+ <Button
+ android:id="@+id/button_finish_this_activity"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Finish this activity" />
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
index 43bea60..9d0c9c3 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
@@ -37,6 +37,17 @@
android:layout_marginBottom="32dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/rear_display_session_button" />
+
+ <Button
+ android:id="@+id/rear_display_session_button"
+ android:text="Get active session if available"
+ 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-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
new file mode 100644
index 0000000..ff5129d
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 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_presentation_button"
+ android:text="Enable Rear Display Presentation"
+ 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-demos/demo/src/main/res/layout/activity_split_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
index ab54e1e..0533be2 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
@@ -30,7 +30,15 @@
android:id="@+id/activity_embedded_status_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Activity is embedded" />
+ android:text="Activity is embedded"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/activity_embedded_bounds_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Embedded bounds not available"
+ android:visibility="gone" />
<CheckBox
android:id="@+id/splitMainCheckBox"
@@ -38,6 +46,40 @@
android:layout_height="wrap_content"
android:text="Split Main with other activities" />
+ <CheckBox
+ android:id="@+id/dividerCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Add a divider between containers" />
+
+ <CheckBox
+ android:id="@+id/draggableDividerCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Make the divider draggable"
+ android:enabled="false"/>
+
+ <CheckBox
+ android:id="@+id/openAnimationJumpCutCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Use jump cut for split open animation"
+ android:enabled="false"/>
+
+ <CheckBox
+ android:id="@+id/closeAnimationJumpCutCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Use jump cut for split close animation"
+ android:enabled="false"/>
+
+ <CheckBox
+ android:id="@+id/changeAnimationJumpCutCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Use jump cut for split change animation"
+ android:enabled="false"/>
+
<View
android:layout_width="match_parent"
android:layout_height="1dp"
@@ -193,5 +235,60 @@
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="Launch Expanded Dialog" />
+
+ <Button
+ android:id="@+id/launch_dialog_activity_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="Launch Dialog Activity" />
+
+ <Button
+ android:id="@+id/launch_dialog_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="Launch Dialog" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA" />
+
+ <Button
+ android:id="@+id/pin_top_activity_stack_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="Pin Top ActivityStack" />
+
+ <CheckBox
+ android:id="@+id/stickyPinRule"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Set Pin Rule Sticky" />
+
+ <Button
+ android:id="@+id/unpin_top_activity_stack_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="Unpin Top ActivityStack" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA" />
+
+ <Button
+ android:id="@+id/button_launch_overlay_associated_activity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="Launch Overlay Associated Activity A" />
</LinearLayout>
</ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
index dad1100..6acd57f 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
@@ -192,6 +192,106 @@
android:visibility="gone"/>
</RadioGroup>
+ <!-- Dropdown for animation background -->
+
+ <View
+ android:id="@+id/animation_background_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/animation_background_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_animation_background"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/animation_background_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for open animation transition -->
+
+ <View
+ android:id="@+id/open_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/open_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_open_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/open_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for close animation transition -->
+
+ <View
+ android:id="@+id/close_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/close_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_close_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/close_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for change animation transition -->
+
+ <View
+ android:id="@+id/change_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/change_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_change_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/change_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
<View
android:id="@+id/finish_secondary_activities_divider"
android:layout_width="match_parent"
@@ -201,6 +301,14 @@
android:visibility="gone"
android:background="#AAAAAA" />
+ <Button
+ android:id="@+id/finish_secondary_activities_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_centerHorizontal="true"
+ android:text="Finish secondary activities"
+ android:visibility="gone"/>
+
<View
android:layout_width="match_parent"
android:layout_height="1dp"
diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
index c71a541..4d6e260 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
@@ -143,6 +143,106 @@
android:text="Swap the position of primary and secondary container" />
</RadioGroup>
+ <!-- Dropdown for animation background -->
+
+ <View
+ android:id="@+id/animation_background_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/animation_background_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_animation_background"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/animation_background_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for open animation transition -->
+
+ <View
+ android:id="@+id/open_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/open_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_open_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/open_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for close animation transition -->
+
+ <View
+ android:id="@+id/close_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/close_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_close_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/close_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
+ <!-- Dropdown for change animation transition -->
+
+ <View
+ android:id="@+id/change_animation_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="#AAAAAA"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/change_animation_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/current_change_animation"
+ android:visibility="gone"/>
+
+ <Spinner
+ android:id="@+id/change_animation_dropdown"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:spinnerMode="dropdown"
+ android:visibility="gone"/>
+
<Button
android:id="@+id/launch_activity_to_side"
android:layout_width="wrap_content"
diff --git a/window/window-demos/demo/src/main/res/layout/window_state_config_view.xml b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
similarity index 62%
rename from window/window-demos/demo/src/main/res/layout/window_state_config_view.xml
rename to window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
index 6a97d8f..9c10df2 100644
--- a/window/window-demos/demo/src/main/res/layout/window_state_config_view.xml
+++ b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
@@ -15,24 +15,20 @@
limitations under the License.
-->
-<LinearLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="5dp"
- android:orientation="horizontal">
+ android:layout_height="match_parent">
<TextView
- android:id="@+id/config_name"
+ android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textStyle="bold"
- android:text="@string/window_state_placeholder"/>
+ android:text="Dual Display Presentation"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
- <TextView
- android:id="@+id/config_value"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/window_state_placeholder"
- android:layout_marginLeft="10dp"/>
-</LinearLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/window_state_view.xml b/window/window-demos/demo/src/main/res/layout/window_state_view.xml
deleted file mode 100644
index 4a03bbd..0000000
--- a/window/window-demos/demo/src/main/res/layout/window_state_view.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2023 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"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <!-- Callback name -->
- <TextView
- android:id="@+id/callback_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@android:style/TextAppearance.Material.Title"
- android:text="@string/window_state_placeholder"/>
-
- <!-- Last update timestamp -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/timestamp_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/timestamp_title"/>
-
- <!-- Application Display rotation -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/application_display_rotation_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/application_display_rotation_title"/>
-
- <!-- Activity Display rotation -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/activity_display_rotation_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/activity_display_rotation_title"/>
-
- <!-- Application Display bounds -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/application_display_bounds_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/application_display_bounds_title"/>
-
- <!-- Activity Display bounds -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/activity_display_bounds_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/activity_display_bounds_title"/>
-
- <!-- Previous Application Display rotation -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/prev_application_display_rotation_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/prev_application_display_rotation_title"/>
-
- <!-- Previous Activity Display rotation -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/prev_activity_display_rotation_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/prev_activity_display_rotation_title"/>
-
- <!-- Previous Application Display bounds -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/prev_application_display_bounds_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/prev_application_display_bounds_title"/>
-
- <!-- Previous Activity Display bounds -->
- <androidx.window.demo.coresdk.WindowStateConfigView
- android:id="@+id/prev_activity_display_bounds_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:configName="@string/prev_activity_display_bounds_title"/>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/values/strings.xml b/window/window-demos/demo/src/main/res/values/strings.xml
index 892cecc..7bc15c3 100644
--- a/window/window-demos/demo/src/main/res/values/strings.xml
+++ b/window/window-demos/demo/src/main/res/values/strings.xml
@@ -50,7 +50,10 @@
<string name="rear_display">Rear Display Mode</string>
<string name="rear_display_description">Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode</string>
<string name="current_split_attributes">Current SplitAttributes:</string>>
- <string name="current_animation_background_color">Current Animation Background Color:</string>>
+ <string name="current_animation_background">Current Animation Background:</string>>
+ <string name="current_open_animation">Current Open Animation:</string>>
+ <string name="current_close_animation">Current Close Animation:</string>>
+ <string name="current_change_animation">Current Change Animation:</string>>
<string name="test_ime">Test IME</string>
<string name="test_ime_button_clear">Clear Logs</string>
<string name="test_ime_button_close">Close Test IME</string>
@@ -61,13 +64,14 @@
<string name="ime_button_settings">System IME Settings</string>
<string name="ime_button_switch_default">Switch default IME</string>
<string name="install_samples_2">Install window-demos:demo-second-app to launch activities from a different UID.</string>
- <string name="toast_split_not_support">Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED and ensure device supports splits.</string>
+ <string name="toast_split_not_available">This device does not support Activity Embedding.</string>
+ <string name="toast_split_not_support">Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED.</string>
<string name="latest_configuration_title">Latest Configuration</string>
<string name="application_display_listener_title">Application DisplayListener#onDisplayChanged</string>
<string name="activity_display_listener_title">Activity DisplayListener#onDisplayChanged</string>
<string name="application_configuration_title">Application#onConfigurationChanged</string>
<string name="activity_configuration_title">Activity#onConfigurationChanged</string>
- <string name="display_feature_title">WindowInfoTracker</string>
+ <string name="display_feature_title">WindowInfoTracker#windowLayoutInfo</string>
<string name="timestamp_title">Timestamp:</string>
<string name="application_display_rotation_title">Application Display Rotation:</string>
<string name="activity_display_rotation_title">Activity Display Rotation:</string>
@@ -86,4 +90,10 @@
<string name="choose_split_type">Choose the split type</string>
<string name="choose_layout_direction">Choose the layout direction</string>
<string name="show_placeholder_warning">Placeholder Activity may show (again) because "Use a placeholder for A is checked". Clear the check box to expand Activity A to fill the task by either clicking "FINISH SECONDARY ACTIVITIES".</string>
+ <string name="dual_display">Dual Display</string>
+ <string name="dual_display_description">Demo of showing content on the rear and internal displays concurrently</string>
+ <string name="toast_show_overlay_warning">The device does not support overlay features!</string>
+ <string name="overlay_bounds_text">Overlay bounds:</string>
+ <string name="width_in_ratio">Width in ratio: </string>
+ <string name="height_in_ratio">Height in ratio: </string>
</resources>
diff --git a/window/window-demos/demo/src/main/res/values/styles.xml b/window/window-demos/demo/src/main/res/values/styles.xml
index 5586114..347966d 100644
--- a/window/window-demos/demo/src/main/res/values/styles.xml
+++ b/window/window-demos/demo/src/main/res/values/styles.xml
@@ -15,15 +15,6 @@
-->
<resources>
-
- <!-- Base application theme. -->
- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
- <!-- Customize your theme here. -->
- <item name="colorPrimary">@color/colorPrimary</item>
- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
- <item name="colorAccent">@color/colorAccent</item>
- </style>
-
<!-- Theme to show the expanded dialog Activity as transparent. -->
<style name="ExpandedDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
<item name="windowNoTitle">true</item>
diff --git a/window/window-demos/demo/src/main/res/xml/main_split_config.xml b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
index 8269c38..3a898ce 100644
--- a/window/window-demos/demo/src/main/res/xml/main_split_config.xml
+++ b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
@@ -41,4 +41,14 @@
<ActivityFilter
window:activityName="androidx.window.demo.embedding.SplitImeActivityMain"/>
</SplitPlaceholderRule>
+
+ <!-- Rules for OverlayActivity -->
+
+ <SplitPairRule
+ window:finishPrimaryWithSecondary="never"
+ window:finishSecondaryWithPrimary="adjacent">
+ <SplitPairFilter
+ window:primaryActivityName="androidx.window.demo.embedding.OverlayActivityA"
+ window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>
+ </SplitPairRule>
</resources>
\ No newline at end of file
diff --git a/window/window-java/api/current.txt b/window/window-java/api/current.txt
index 2e19128..c3b2d91 100644
--- a/window/window-java/api/current.txt
+++ b/window/window-java/api/current.txt
@@ -11,6 +11,18 @@
package androidx.window.java.embedding {
+ public final class ActivityEmbeddingControllerCallbackAdapter {
+ ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+ }
+
+ public final class OverlayControllerCallbackAdapter {
+ ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+ }
+
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
diff --git a/window/window-java/api/restricted_current.txt b/window/window-java/api/restricted_current.txt
index 2e19128..c3b2d91 100644
--- a/window/window-java/api/restricted_current.txt
+++ b/window/window-java/api/restricted_current.txt
@@ -11,6 +11,18 @@
package androidx.window.java.embedding {
+ public final class ActivityEmbeddingControllerCallbackAdapter {
+ ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+ }
+
+ public final class OverlayControllerCallbackAdapter {
+ ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+ }
+
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
new file mode 100644
index 0000000..0f0e7f2
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.app.Activity
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.EmbeddedActivityWindowInfo
+import androidx.window.java.core.CallbackToFlowAdapter
+import java.util.concurrent.Executor
+
+/**
+ * An adapted interface for [ActivityEmbeddingController] that provides callback shaped APIs to
+ * report the latest [EmbeddedActivityWindowInfo].
+ *
+ * It should only be used if [ActivityEmbeddingController.embeddedActivityWindowInfo] is not
+ * available. For example, an app is written in Java and cannot use Flow APIs.
+ *
+ * @param controller an [ActivityEmbeddingController] that can be obtained by
+ * [ActivityEmbeddingController.getInstance].
+ * @constructor creates a callback adapter of
+ * [ActivityEmbeddingController.embeddedActivityWindowInfo] flow API.
+ */
+class ActivityEmbeddingControllerCallbackAdapter(
+ private val controller: ActivityEmbeddingController
+) {
+ private val callbackToFlowAdapter = CallbackToFlowAdapter()
+
+ /**
+ * Registers a listener for updates of [EmbeddedActivityWindowInfo] of [activity].
+ *
+ * The [listener] will immediately be invoked with the latest value upon registration if the
+ * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
+ *
+ * When the [activity] is embedded, the [listener] will be invoked when
+ * [EmbeddedActivityWindowInfo] is changed. When the [activity] is not embedded, the [listener]
+ * will not be triggered unless the [activity] is becoming non-embedded from embedded.
+ *
+ * Note that this API is only supported on the device with
+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+ * [WindowSdkExtensions.extensionVersion] is less than 6, this [listener] will not be invoked.
+ *
+ * @param activity the [Activity] that is interested in getting the embedded window info.
+ * @param executor the [Executor] to dispatch the [EmbeddedActivityWindowInfo] change.
+ * @param listener the [Consumer] that will be invoked on the [executor] when there is an update
+ * to [EmbeddedActivityWindowInfo].
+ */
+ @RequiresWindowSdkExtension(6)
+ fun addEmbeddedActivityWindowInfoListener(
+ activity: Activity,
+ executor: Executor,
+ listener: Consumer<EmbeddedActivityWindowInfo>
+ ) {
+ callbackToFlowAdapter.connect(
+ executor,
+ listener,
+ controller.embeddedActivityWindowInfo(activity)
+ )
+ }
+
+ /**
+ * Unregisters a listener that was previously registered via
+ * [addEmbeddedActivityWindowInfoListener].
+ *
+ * It's no-op if the [listener] has not been registered.
+ *
+ * @param listener the previously registered [Consumer] to unregister.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun removeEmbeddedActivityWindowInfoListener(listener: Consumer<EmbeddedActivityWindowInfo>) {
+ callbackToFlowAdapter.disconnect(listener)
+ }
+}
diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
new file mode 100644
index 0000000..78d9e13
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 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.embedding
+
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
+import androidx.window.java.core.CallbackToFlowAdapter
+import java.util.concurrent.Executor
+
+/**
+ * An adapted interface for [OverlayController] that provides callback shaped APIs to report the
+ * latest [OverlayInfo].
+ *
+ * It should only be used if [OverlayController.overlayInfo] is not available. For example, an app
+ * is written in Java and cannot use Flow APIs.
+ *
+ * @param controller an [OverlayController] that can be obtained by [OverlayController.getInstance].
+ * @constructor creates a callback adapter of [OverlayController.overlayInfo] flow API.
+ */
+class OverlayControllerCallbackAdapter(private val controller: OverlayController) {
+
+ private val callbackToFlowAdapter = CallbackToFlowAdapter()
+
+ /**
+ * Registers a listener for updates of [OverlayInfo] that [overlayTag] is associated with.
+ *
+ * If there is no active overlay [ActivityStack], the reported [OverlayInfo.activityStack] and
+ * [OverlayInfo.currentOverlayAttributes] will be `null`.
+ *
+ * Note that launching an overlay [ActivityStack] only supports on the device with
+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 5. If
+ * [WindowSdkExtensions.extensionVersion] is less than 5, this flow will always report
+ * [OverlayInfo] without associated [OverlayInfo.activityStack].
+ *
+ * @param overlayTag the overlay [ActivityStack]'s tag which is set through
+ * [OverlayCreateParams]
+ * @param executor the [Executor] to dispatch the [OverlayInfo] change
+ * @param consumer the [Consumer] that will be invoked on the [executor] when there is an update
+ * to [OverlayInfo].
+ */
+ @RequiresWindowSdkExtension(5)
+ fun addOverlayInfoListener(
+ overlayTag: String,
+ executor: Executor,
+ consumer: Consumer<OverlayInfo>
+ ) {
+ callbackToFlowAdapter.connect(executor, consumer, controller.overlayInfo(overlayTag))
+ }
+
+ /**
+ * Unregisters a listener that was previously registered via [addOverlayInfoListener].
+ *
+ * @param consumer the previously registered [Consumer] to unregister.
+ */
+ @RequiresWindowSdkExtension(5)
+ fun removeOverlayInfoListener(consumer: Consumer<OverlayInfo>) {
+ callbackToFlowAdapter.disconnect(consumer)
+ }
+}
diff --git a/window/window-testing/api/current.txt b/window/window-testing/api/current.txt
index 847a9e8..84d25ac 100644
--- a/window/window-testing/api/current.txt
+++ b/window/window-testing/api/current.txt
@@ -1,4 +1,14 @@
// Signature format: 4.0
+package androidx.window.testing {
+
+ public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
+ ctor public WindowSdkExtensionsRule();
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+ method public void overrideExtensionVersion(@IntRange(from=0L) int version);
+ }
+
+}
+
package androidx.window.testing.embedding {
public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
diff --git a/window/window-testing/api/restricted_current.txt b/window/window-testing/api/restricted_current.txt
index 847a9e8..84d25ac 100644
--- a/window/window-testing/api/restricted_current.txt
+++ b/window/window-testing/api/restricted_current.txt
@@ -1,4 +1,14 @@
// Signature format: 4.0
+package androidx.window.testing {
+
+ public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
+ ctor public WindowSdkExtensionsRule();
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+ method public void overrideExtensionVersion(@IntRange(from=0L) int version);
+ }
+
+}
+
package androidx.window.testing.embedding {
public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
diff --git a/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
new file mode 100644
index 0000000..75ebdf5
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.testing
+
+import androidx.annotation.IntRange
+import androidx.window.WindowSdkExtensions
+
+/**
+ * A fake [WindowSdkExtensions] implementation that can override [extensionVersion], which is
+ * intended to be used during unit tests.
+ */
+internal class FakeWindowSdkExtensions : WindowSdkExtensions() {
+
+ override val extensionVersion: Int
+ get() = _extensionVersion
+
+ private var _extensionVersion: Int = 0
+
+ internal fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+ require(version >= 0) { "The override version must equal to or greater than 0." }
+ _extensionVersion = version
+ }
+}
diff --git a/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
new file mode 100644
index 0000000..185fdae
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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.testing
+
+import androidx.annotation.IntRange
+import androidx.window.WindowSdkExtensions
+import androidx.window.WindowSdkExtensionsDecorator
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * [TestRule] for overriding [WindowSdkExtensions] properties in unit tests.
+ *
+ * The [TestRule] is designed to only be used in unit tests. Users should use the actual
+ * [WindowSdkExtensions] properties for instrumentation tests. Overriding the device's extensions
+ * version to a higher version may lead to unexpected test failures or even app crash.
+ */
+class WindowSdkExtensionsRule : TestRule {
+
+ private val fakeWindowSdkExtensions = FakeWindowSdkExtensions()
+
+ override fun apply(
+ @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+ base: Statement,
+ @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+ description: Description
+ ): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ WindowSdkExtensions.overrideDecorator(
+ object : WindowSdkExtensionsDecorator {
+ override fun decorate(
+ windowSdkExtensions: WindowSdkExtensions
+ ): WindowSdkExtensions = fakeWindowSdkExtensions
+ }
+ )
+ try {
+ base.evaluate()
+ } finally {
+ WindowSdkExtensions.reset()
+ }
+ }
+ }
+ }
+
+ /**
+ * Overrides the [WindowSdkExtensions.extensionVersion] for testing.
+ *
+ * @param version The extension version to override.
+ */
+ fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+ fakeWindowSdkExtensions.overrideExtensionVersion(version)
+ }
+}
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
index 7c62b4f..e0c3480 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
@@ -18,9 +18,6 @@
package androidx.window.testing.embedding
import android.app.Activity
-import android.os.Binder
-import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
import androidx.window.embedding.ActivityStack
/**
@@ -41,8 +38,3 @@
activitiesInProcess: List<Activity> = emptyList(),
isEmpty: Boolean = false,
): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@VisibleForTesting
-@JvmField
-val TEST_ACTIVITY_STACK_TOKEN = Binder()
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
index 5195f28..2cabe03 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
@@ -17,7 +17,6 @@
package androidx.window.testing.embedding
-import android.os.Binder
import androidx.window.embedding.ActivityStack
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitInfo
@@ -44,6 +43,8 @@
secondActivityStack: ActivityStack = TestActivityStack(),
splitAttributes: SplitAttributes = SplitAttributes.Builder().build(),
): SplitInfo =
- SplitInfo(primaryActivityStack, secondActivityStack, splitAttributes, TEST_SPLIT_INFO_TOKEN)
-
-private val TEST_SPLIT_INFO_TOKEN = Binder()
+ SplitInfo(
+ primaryActivityStack,
+ secondActivityStack,
+ splitAttributes,
+ )
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
index 473e3d9..dcd3a23 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
@@ -17,17 +17,24 @@
package androidx.window.testing.embedding
import android.app.Activity
-import android.app.ActivityOptions
-import android.os.IBinder
+import android.os.Bundle
import androidx.core.util.Consumer
+import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.EmbeddedActivityWindowInfo
import androidx.window.embedding.EmbeddingBackend
+import androidx.window.embedding.EmbeddingConfiguration
import androidx.window.embedding.EmbeddingRule
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayAttributesCalculatorParams
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributesCalculatorParams
import androidx.window.embedding.SplitController
import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
import androidx.window.embedding.SplitInfo
+import androidx.window.embedding.SplitPinRule
import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -149,6 +156,15 @@
override fun isActivityEmbedded(activity: Activity): Boolean =
embeddedActivities.contains(activity)
+ @OptIn(ExperimentalWindowApi::class)
+ override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun unpinTopActivityStack(taskId: Int) {
+ TODO("Not yet implemented")
+ }
+
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
@@ -164,13 +180,29 @@
}
override fun setLaunchingActivityStack(
- options: ActivityOptions,
- token: IBinder
- ): ActivityOptions {
+ options: Bundle,
+ activityStack: ActivityStack,
+ ): Bundle {
TODO("Not yet implemented")
}
- override fun invalidateTopVisibleSplitAttributes() {
+ override fun setOverlayCreateParams(
+ options: Bundle,
+ overlayCreateParams: OverlayCreateParams
+ ): Bundle {
+ TODO("Not yet implemented")
+ }
+
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ TODO("Not yet implemented")
+ }
+
+ @OptIn(ExperimentalWindowApi::class)
+ override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+ TODO("Not yet implemented")
+ }
+
+ override fun invalidateVisibleActivityStacks() {
TODO("Not yet implemented")
}
@@ -178,6 +210,45 @@
TODO("Not yet implemented")
}
+ override fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun clearOverlayAttributesCalculator() {
+ TODO("Not yet implemented")
+ }
+
+ override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+ TODO("Not yet implemented")
+ }
+
+ override fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: Consumer<OverlayInfo>
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>) {
+ TODO("Not yet implemented")
+ }
+
+ override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+ activity: Activity,
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ ) {
+ TODO("Not yet implemented")
+ }
+
private fun validateRules(rules: Set<EmbeddingRule>) {
val tags = HashSet<String>()
rules.forEach { rule ->
diff --git a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
index f59e5ae..5d7d07a 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
@@ -39,13 +39,13 @@
override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
val displayMetrics = activity.resources.displayMetrics
val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
- return WindowMetrics(bounds)
+ return WindowMetrics(bounds, density = displayMetrics.density)
}
override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
val displayMetrics = activity.resources.displayMetrics
val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
- return WindowMetrics(bounds)
+ return WindowMetrics(bounds, density = displayMetrics.density)
}
// WindowManager#getDefaultDisplay is deprecated but we have this for compatibility with
@@ -53,9 +53,10 @@
@Suppress("DEPRECATION")
override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val density = context.resources.displayMetrics.density
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Api30Impl.getWindowMetrics(wm)
+ Api30Impl.getWindowMetrics(wm, context)
} else {
val displaySize = Point()
// We use getRealSize instead of getSize here because:
@@ -67,7 +68,7 @@
// getRealSize.
wm.defaultDisplay.getRealSize(displaySize)
val bounds = Rect(0, 0, displaySize.x, displaySize.y)
- WindowMetrics(bounds)
+ WindowMetrics(bounds, density = density)
}
}
@@ -77,8 +78,14 @@
@RequiresApi(Build.VERSION_CODES.R)
private object Api30Impl {
- fun getWindowMetrics(windowManager: WindowManager): WindowMetrics {
- return WindowMetrics(windowManager.currentWindowMetrics.bounds)
+ fun getWindowMetrics(
+ windowManager: WindowManager,
+ @UiContext context: Context
+ ): WindowMetrics {
+ return WindowMetrics(
+ windowManager.currentWindowMetrics.bounds,
+ density = context.resources.displayMetrics.density
+ )
}
}
}
diff --git a/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
new file mode 100644
index 0000000..24dc179
--- /dev/null
+++ b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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.testing
+
+import androidx.window.WindowSdkExtensions
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+/** Test class to verify [WindowSdkExtensionsRule] behaviors. */
+class WindowSdkExtensionsRuleTest {
+
+ @JvmField @Rule val rule = WindowSdkExtensionsRule()
+
+ /** Verifies the [WindowSdkExtensionsRule] behavior. */
+ @Test
+ fun testWindowSdkExtensionsRule() {
+ assertEquals(
+ "The WindowSdkExtensions.extensionVersion is 0 in unit test",
+ 0,
+ WindowSdkExtensions.getInstance().extensionVersion
+ )
+
+ rule.overrideExtensionVersion(3)
+
+ assertEquals(3, WindowSdkExtensions.getInstance().extensionVersion)
+ }
+}
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
index ca3d9c4..d4df0a3 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
@@ -40,8 +40,10 @@
public void testActivityStackDefaultValue() {
final ActivityStack activityStack = TestActivityStack.createTestActivityStack();
- assertEquals(new ActivityStack(Collections.emptyList(), false /* isEmpty */),
- activityStack);
+ assertEquals(
+ new ActivityStack(Collections.emptyList(), false /* isEmpty */),
+ activityStack
+ );
}
/** Verifies {@link TestActivityStack} */
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
index 27e70c26..a797045 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
@@ -52,7 +52,7 @@
public class SplitAttributesCalculatorParamsTestingJavaTest {
private static final Rect TEST_BOUNDS = new Rect(0, 0, 2000, 2000);
private static final WindowMetrics TEST_METRICS = new WindowMetrics(TEST_BOUNDS,
- WindowInsetsCompat.CONSUMED);
+ WindowInsetsCompat.CONSUMED, 1f /* density */);
private static final SplitAttributes DEFAULT_SPLIT_ATTRIBUTES =
new SplitAttributes.Builder().build();
private static final SplitAttributes TABLETOP_HINGE_ATTRIBUTES = new SplitAttributes.Builder()
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
index 2e90c35..1a4eed3 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
@@ -102,7 +102,7 @@
companion object {
private val TEST_BOUNDS = Rect(0, 0, 2000, 2000)
- private val TEST_METRICS = WindowMetrics(TEST_BOUNDS)
+ private val TEST_METRICS = WindowMetrics(TEST_BOUNDS, density = 1f)
private val DEFAULT_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
private val TABLETOP_HINGE_ATTRIBUTES =
SplitAttributes.Builder()
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
index f005d0e..ef2f690 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
@@ -16,14 +16,12 @@
package androidx.window.testing.embedding
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitAttributes
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.kotlin.mock
/** Test class to verify [TestSplitInfo] */
-@OptIn(ExperimentalWindowApi::class)
class SplitInfoTestingTest {
/** Verifies the default value of [TestSplitInfo]. */
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
deleted file mode 100644
index ca6efd7..0000000
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2023 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.testing.embedding
-
-import android.app.Activity
-import android.os.Binder
-import android.os.IBinder
-import androidx.window.core.ExperimentalWindowApi
-import androidx.window.embedding.ActivityStack
-import androidx.window.embedding.SplitAttributes
-import androidx.window.embedding.SplitInfo
-
-/**
- * A convenience method to get a test [SplitInfo] with default values provided. With the default
- * values it returns an empty [ActivityStack] for the primary and secondary stacks. The default
- * [SplitAttributes] are for splitting equally and matching the locale layout.
- *
- * Note: This method should be used for testing local logic as opposed to end to end verification.
- * End to end verification requires a device that supports Activity Embedding.
- *
- * @param primaryActivity the [Activity] for the primary container.
- * @param secondaryActivity the [Activity] for the secondary container.
- * @param splitAttributes the [SplitAttributes].
- */
-@ExperimentalWindowApi
-fun TestSplitInfo(
- primaryActivity: Activity,
- secondaryActivity: Activity,
- splitAttributes: SplitAttributes = SplitAttributes(),
- token: IBinder = Binder()
-): SplitInfo {
- val primaryActivityStack = TestActivityStack(primaryActivity, false)
- val secondaryActivityStack = TestActivityStack(secondaryActivity, false)
- return SplitInfo(primaryActivityStack, secondaryActivityStack, splitAttributes, token)
-}
-
-/**
- * A convenience method to get a test [ActivityStack] with default values provided. With the default
- * values, there will be a single [Activity] in the stack and it will be considered not empty.
- *
- * Note: This method should be used for testing local logic as opposed to end to end verification.
- * End to end verification requires a device that supports Activity Embedding.
- *
- * @param testActivity an [Activity] that should be considered in the stack
- * @param isEmpty states if the stack is empty or not. In practice an [ActivityStack] with a single
- * [Activity] but [isEmpty] set to `false` means there is an [Activity] from outside the process
- * in the stack.
- */
-@ExperimentalWindowApi
-fun TestActivityStack(
- testActivity: Activity,
- isEmpty: Boolean = true,
-): ActivityStack {
- return ActivityStack(
- listOf(testActivity),
- isEmpty,
- )
-}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index f68c635..ff947e3 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -13,6 +13,8 @@
field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
}
public abstract class WindowSdkExtensions {
@@ -107,8 +109,10 @@
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
method public android.content.Context getContext();
+ method public android.view.Window? getWindow();
method public void setContentView(android.view.View view);
property public abstract android.content.Context context;
+ property public abstract android.view.Window? window;
}
}
@@ -123,9 +127,13 @@
package androidx.window.embedding {
public final class ActivityEmbeddingController {
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.EmbeddedActivityWindowInfo> embeddedActivityWindowInfo(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+ method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
method public boolean isActivityEmbedded(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
}
@@ -133,6 +141,10 @@
method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
}
+ public final class ActivityEmbeddingOptions {
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+ }
+
public final class ActivityFilter {
ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
method public android.content.ComponentName getComponentName();
@@ -163,6 +175,121 @@
property public final boolean isEmpty;
}
+ public abstract class DividerAttributes {
+ method public final int getColor();
+ method public final int getWidthDp();
+ property public final int color;
+ property public final int widthDp;
+ field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
+ field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Companion {
+ }
+
+ public abstract static class DividerAttributes.DragRange {
+ field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
+ field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
+ }
+
+ public static final class DividerAttributes.DragRange.Companion {
+ }
+
+ public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
+ ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
+ method public float getMaxRatio();
+ method public float getMinRatio();
+ property public final float maxRatio;
+ property public final float minRatio;
+ }
+
+ public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
+ method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
+ method public boolean isDraggingToFullscreenAllowed();
+ property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
+ property public final boolean isDraggingToFullscreenAllowed;
+ }
+
+ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
+ ctor public DividerAttributes.DraggableDividerAttributes.Builder();
+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
+ method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDraggingToFullscreenAllowed(boolean allowed);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+ }
+
+ public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
+ }
+
+ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
+ ctor public DividerAttributes.FixedDividerAttributes.Builder();
+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+ }
+
+ public final class EmbeddedActivityWindowInfo {
+ method public android.graphics.Rect getBoundsInParentHost();
+ method public android.graphics.Rect getParentHostBounds();
+ method public boolean isEmbedded();
+ property public final android.graphics.Rect boundsInParentHost;
+ property public final boolean isEmbedded;
+ property public final android.graphics.Rect parentHostBounds;
+ }
+
+ public abstract class EmbeddingAnimationBackground {
+ method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+ field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
+ }
+
+ public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
+ method public int getColor();
+ property public final int color;
+ }
+
+ public static final class EmbeddingAnimationBackground.Companion {
+ method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+ }
+
+ public final class EmbeddingAnimationParams {
+ ctor public EmbeddingAnimationParams();
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation);
+ method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getChangeAnimation();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getCloseAnimation();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getOpenAnimation();
+ property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation;
+ }
+
+ public static final class EmbeddingAnimationParams.AnimationSpec {
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec DEFAULT;
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec JUMP_CUT;
+ }
+
+ public static final class EmbeddingAnimationParams.AnimationSpec.Companion {
+ }
+
+ public static final class EmbeddingAnimationParams.Builder {
+ ctor public EmbeddingAnimationParams.Builder();
+ method public androidx.window.embedding.EmbeddingAnimationParams build();
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setChangeAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setCloseAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setOpenAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ }
+
public final class EmbeddingAspectRatio {
method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
@@ -174,6 +301,29 @@
method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
}
+ public final class EmbeddingConfiguration {
+ ctor public EmbeddingConfiguration();
+ ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
+ method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
+ property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
+ }
+
+ public static final class EmbeddingConfiguration.Builder {
+ ctor public EmbeddingConfiguration.Builder();
+ method public androidx.window.embedding.EmbeddingConfiguration build();
+ method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
+ }
+
+ public static final class EmbeddingConfiguration.DimAreaBehavior {
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
+ }
+
+ public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
+ }
+
public abstract class EmbeddingRule {
method public final String? getTag();
property public final String? tag;
@@ -196,8 +346,21 @@
}
public final class SplitAttributes {
+ ctor public SplitAttributes();
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
+ ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+ ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground, androidx.window.embedding.DividerAttributes dividerAttributes);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams, optional androidx.window.embedding.DividerAttributes dividerAttributes);
+ method @Deprecated public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+ method public androidx.window.embedding.EmbeddingAnimationParams getAnimationParams();
+ method public androidx.window.embedding.DividerAttributes getDividerAttributes();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+ property @Deprecated public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+ property public final androidx.window.embedding.EmbeddingAnimationParams animationParams;
+ property public final androidx.window.embedding.DividerAttributes dividerAttributes;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
@@ -206,6 +369,9 @@
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
+ method @Deprecated @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+ method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.embedding.EmbeddingAnimationParams params);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
@@ -256,10 +422,11 @@
method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
field public static final androidx.window.embedding.SplitController.Companion Companion;
}
@@ -326,6 +493,24 @@
method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
}
+ public final class SplitPinRule extends androidx.window.embedding.SplitRule {
+ method public boolean isSticky();
+ property public final boolean isSticky;
+ }
+
+ public static final class SplitPinRule.Builder {
+ ctor public SplitPinRule.Builder();
+ method public androidx.window.embedding.SplitPinRule build();
+ method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
+ method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
+ }
+
public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
@@ -431,10 +616,20 @@
public static final class FoldingFeature.State.Companion {
}
+ public final class SupportedPosture {
+ field public static final androidx.window.layout.SupportedPosture.Companion Companion;
+ field public static final androidx.window.layout.SupportedPosture TABLETOP;
+ }
+
+ public static final class SupportedPosture.Companion {
+ }
+
public interface WindowInfoTracker {
method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+ method public default java.util.List<androidx.window.layout.SupportedPosture> getSupportedPostures();
method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
+ property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List<androidx.window.layout.SupportedPosture> supportedPostures;
field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
}
@@ -449,8 +644,10 @@
public final class WindowMetrics {
method public android.graphics.Rect getBounds();
+ method public float getDensity();
method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
property public final android.graphics.Rect bounds;
+ property public final float density;
}
public interface WindowMetricsCalculator {
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index f68c635..5b2213b 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -13,6 +13,8 @@
field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
}
public abstract class WindowSdkExtensions {
@@ -107,8 +109,10 @@
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
method public android.content.Context getContext();
+ method public android.view.Window? getWindow();
method public void setContentView(android.view.View view);
property public abstract android.content.Context context;
+ property public abstract android.view.Window? window;
}
}
@@ -123,9 +127,13 @@
package androidx.window.embedding {
public final class ActivityEmbeddingController {
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.EmbeddedActivityWindowInfo> embeddedActivityWindowInfo(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+ method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
method public boolean isActivityEmbedded(android.app.Activity activity);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
}
@@ -133,6 +141,11 @@
method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
}
+ public final class ActivityEmbeddingOptions {
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);
+ }
+
public final class ActivityFilter {
ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
method public android.content.ComponentName getComponentName();
@@ -163,6 +176,121 @@
property public final boolean isEmpty;
}
+ public abstract class DividerAttributes {
+ method public final int getColor();
+ method public final int getWidthDp();
+ property public final int color;
+ property public final int widthDp;
+ field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
+ field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Companion {
+ }
+
+ public abstract static class DividerAttributes.DragRange {
+ field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
+ field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
+ }
+
+ public static final class DividerAttributes.DragRange.Companion {
+ }
+
+ public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
+ ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
+ method public float getMaxRatio();
+ method public float getMinRatio();
+ property public final float maxRatio;
+ property public final float minRatio;
+ }
+
+ public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
+ method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
+ method public boolean isDraggingToFullscreenAllowed();
+ property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
+ property public final boolean isDraggingToFullscreenAllowed;
+ }
+
+ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
+ ctor public DividerAttributes.DraggableDividerAttributes.Builder();
+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
+ method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDraggingToFullscreenAllowed(boolean allowed);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+ }
+
+ public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
+ }
+
+ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
+ ctor public DividerAttributes.FixedDividerAttributes.Builder();
+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+ }
+
+ public final class EmbeddedActivityWindowInfo {
+ method public android.graphics.Rect getBoundsInParentHost();
+ method public android.graphics.Rect getParentHostBounds();
+ method public boolean isEmbedded();
+ property public final android.graphics.Rect boundsInParentHost;
+ property public final boolean isEmbedded;
+ property public final android.graphics.Rect parentHostBounds;
+ }
+
+ public abstract class EmbeddingAnimationBackground {
+ method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+ field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
+ }
+
+ public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
+ method public int getColor();
+ property public final int color;
+ }
+
+ public static final class EmbeddingAnimationBackground.Companion {
+ method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+ }
+
+ public final class EmbeddingAnimationParams {
+ ctor public EmbeddingAnimationParams();
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation);
+ ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation);
+ method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getChangeAnimation();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getCloseAnimation();
+ method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getOpenAnimation();
+ property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation;
+ property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation;
+ }
+
+ public static final class EmbeddingAnimationParams.AnimationSpec {
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec DEFAULT;
+ field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec JUMP_CUT;
+ }
+
+ public static final class EmbeddingAnimationParams.AnimationSpec.Companion {
+ }
+
+ public static final class EmbeddingAnimationParams.Builder {
+ ctor public EmbeddingAnimationParams.Builder();
+ method public androidx.window.embedding.EmbeddingAnimationParams build();
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setChangeAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setCloseAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ method public androidx.window.embedding.EmbeddingAnimationParams.Builder setOpenAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+ }
+
public final class EmbeddingAspectRatio {
method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
@@ -174,11 +302,149 @@
method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EmbeddingBounds {
+ ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);
+ method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();
+ method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();
+ method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();
+ property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;
+ property public final androidx.window.embedding.EmbeddingBounds.Dimension height;
+ property public final androidx.window.embedding.EmbeddingBounds.Dimension width;
+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;
+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;
+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;
+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;
+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;
+ field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;
+ }
+
+ public static final class EmbeddingBounds.Alignment {
+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;
+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;
+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;
+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;
+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;
+ }
+
+ public static final class EmbeddingBounds.Alignment.Companion {
+ }
+
+ public static final class EmbeddingBounds.Companion {
+ }
+
+ public abstract static class EmbeddingBounds.Dimension {
+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;
+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;
+ }
+
+ public static final class EmbeddingBounds.Dimension.Companion {
+ method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
+ method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
+ }
+
+ public final class EmbeddingConfiguration {
+ ctor public EmbeddingConfiguration();
+ ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
+ method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
+ property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
+ }
+
+ public static final class EmbeddingConfiguration.Builder {
+ ctor public EmbeddingConfiguration.Builder();
+ method public androidx.window.embedding.EmbeddingConfiguration build();
+ method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
+ }
+
+ public static final class EmbeddingConfiguration.DimAreaBehavior {
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
+ }
+
+ public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
+ }
+
public abstract class EmbeddingRule {
method public final String? getTag();
property public final String? tag;
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayAttributes {
+ ctor public OverlayAttributes();
+ ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);
+ method public androidx.window.embedding.EmbeddingBounds getBounds();
+ property public final androidx.window.embedding.EmbeddingBounds bounds;
+ }
+
+ public static final class OverlayAttributes.Builder {
+ ctor public OverlayAttributes.Builder();
+ method public androidx.window.embedding.OverlayAttributes build();
+ method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayAttributesCalculatorParams {
+ method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();
+ method public String getOverlayTag();
+ method public android.content.res.Configuration getParentConfiguration();
+ method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+ method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+ property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;
+ property public final String overlayTag;
+ property public final android.content.res.Configuration parentConfiguration;
+ property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+ property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayController {
+ method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void clearOverlayAttributesCalculator();
+ method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);
+ method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.OverlayInfo> overlayInfo(String overlayTag);
+ method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.OverlayAttributesCalculatorParams,androidx.window.embedding.OverlayAttributes> calculator);
+ method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);
+ field public static final androidx.window.embedding.OverlayController.Companion Companion;
+ }
+
+ public static final class OverlayController.Companion {
+ method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayCreateParams {
+ ctor public OverlayCreateParams();
+ ctor public OverlayCreateParams(optional String tag);
+ ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);
+ method public static String generateOverlayTag();
+ method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();
+ method public String getTag();
+ property public final androidx.window.embedding.OverlayAttributes overlayAttributes;
+ property public final String tag;
+ field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;
+ }
+
+ public static final class OverlayCreateParams.Builder {
+ ctor public OverlayCreateParams.Builder();
+ method public androidx.window.embedding.OverlayCreateParams build();
+ method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);
+ method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);
+ }
+
+ public static final class OverlayCreateParams.Companion {
+ method public String generateOverlayTag();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayInfo {
+ method public operator boolean contains(android.app.Activity activity);
+ method public androidx.window.embedding.ActivityStack? getActivityStack();
+ method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();
+ method public String getOverlayTag();
+ property public final androidx.window.embedding.ActivityStack? activityStack;
+ property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;
+ property public final String overlayTag;
+ }
+
public final class RuleController {
method public void addRule(androidx.window.embedding.EmbeddingRule rule);
method public void clearRules();
@@ -196,8 +462,21 @@
}
public final class SplitAttributes {
+ ctor public SplitAttributes();
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
+ ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+ ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground, androidx.window.embedding.DividerAttributes dividerAttributes);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams);
+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams, optional androidx.window.embedding.DividerAttributes dividerAttributes);
+ method @Deprecated public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+ method public androidx.window.embedding.EmbeddingAnimationParams getAnimationParams();
+ method public androidx.window.embedding.DividerAttributes getDividerAttributes();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+ property @Deprecated public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+ property public final androidx.window.embedding.EmbeddingAnimationParams animationParams;
+ property public final androidx.window.embedding.DividerAttributes dividerAttributes;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
@@ -206,6 +485,9 @@
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
+ method @Deprecated @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+ method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.embedding.EmbeddingAnimationParams params);
+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
@@ -256,10 +538,11 @@
method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
field public static final androidx.window.embedding.SplitController.Companion Companion;
}
@@ -326,6 +609,24 @@
method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
}
+ public final class SplitPinRule extends androidx.window.embedding.SplitRule {
+ method public boolean isSticky();
+ property public final boolean isSticky;
+ }
+
+ public static final class SplitPinRule.Builder {
+ ctor public SplitPinRule.Builder();
+ method public androidx.window.embedding.SplitPinRule build();
+ method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
+ method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
+ method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
+ }
+
public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
@@ -431,10 +732,20 @@
public static final class FoldingFeature.State.Companion {
}
+ public final class SupportedPosture {
+ field public static final androidx.window.layout.SupportedPosture.Companion Companion;
+ field public static final androidx.window.layout.SupportedPosture TABLETOP;
+ }
+
+ public static final class SupportedPosture.Companion {
+ }
+
public interface WindowInfoTracker {
method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+ method public default java.util.List<androidx.window.layout.SupportedPosture> getSupportedPostures();
method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
+ property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List<androidx.window.layout.SupportedPosture> supportedPostures;
field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
}
@@ -449,8 +760,10 @@
public final class WindowMetrics {
method public android.graphics.Rect getBounds();
+ method public float getDensity();
method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
property public final android.graphics.Rect bounds;
+ property public final float density;
}
public interface WindowMetricsCalculator {
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 90908f3..63085d7 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -54,13 +54,12 @@
implementation("androidx.core:core:1.8.0")
def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
- def extensions_version = "androidx.window.extensions:extensions:1.2.0"
+ def extensions_version = project(":window:extensions:extensions")
// A compile only dependency on extnensions.core so that other libraries do not expose it
// transitively.
compileOnly(extensions_core_version)
- // A compile only dependency on extnensions.core so that other libraries do not expose it
- // transitively. The testCompile is added because tests are not getting the dependency
- // transitively.
+ // Test implementation is required since extensions:core is on device. So it is required to
+ // import it in some form. For the library it will be available on device.
testImplementation(extensions_core_version)
// A compile only dependency on extnensions.core so that other libraries do not expose it
// transitively. The androidTestCompile is added because tests are not getting the dependency
@@ -79,7 +78,6 @@
testImplementation(libs.kotlinCoroutinesTest)
testImplementation(extensions_version)
testImplementation(compileOnly(project(":window:sidecar:sidecar")))
- testImplementation(compileOnly(extensions_version))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.kotlinTestJunit)
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
new file mode 100644
index 0000000..615adab
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 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.samples.embedding
+
+import android.app.Activity
+import androidx.annotation.Sampled
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.SplitController
+
+@Sampled
+suspend fun expandPrimaryContainer() {
+ SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity).collect {
+ splitInfoList ->
+ // Find all associated secondary ActivityStacks
+ val associatedSecondaryActivityStacks =
+ splitInfoList.mapTo(mutableSetOf()) { splitInfo -> splitInfo.secondaryActivityStack }
+ // Finish them all.
+ ActivityEmbeddingController.getInstance(primaryActivity)
+ .finishActivityStacks(associatedSecondaryActivityStacks)
+ }
+}
+
+val primaryActivity = Activity()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
new file mode 100644
index 0000000..be84e59
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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.samples.embedding
+
+import android.app.Activity
+import androidx.annotation.Sampled
+import androidx.core.app.ActivityOptionsCompat
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.SplitController
+import androidx.window.embedding.setLaunchingActivityStack
+
+@Sampled
+suspend fun launchingOnPrimaryActivityStack() {
+ var primaryActivityStack: ActivityStack? = null
+
+ SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity).collect {
+ splitInfoList ->
+ primaryActivityStack = splitInfoList.last().primaryActivityStack
+ }
+
+ primaryActivity.startActivity(
+ INTENT,
+ ActivityOptionsCompat.makeBasic()
+ .toBundle()!!
+ .setLaunchingActivityStack(primaryActivity, primaryActivityStack!!)
+ )
+}
+
+@Sampled
+suspend fun launchingOnOverlayActivityStack() {
+ var overlayActivityStack: ActivityStack? = null
+
+ OverlayController.getInstance(context).overlayInfo(TAG_OVERLAY).collect { overlayInfo ->
+ overlayActivityStack = overlayInfo.activityStack
+ }
+
+ // The use case is to launch an Activity to an existing overlay ActivityStack from the overlain
+ // Activity. If activityStack is not specified, the activity is launched to the top of the
+ // host task behind the overlay ActivityStack.
+ overlainActivity.startActivity(
+ INTENT,
+ ActivityOptionsCompat.makeBasic()
+ .toBundle()!!
+ .setLaunchingActivityStack(overlainActivity, overlayActivityStack!!)
+ )
+}
+
+const val TAG_OVERLAY = "overlay"
+val overlainActivity = Activity()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
new file mode 100644
index 0000000..b4e9b12
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 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.samples.embedding
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Rect
+import androidx.annotation.Sampled
+import androidx.core.app.ActivityOptionsCompat
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.setOverlayCreateParams
+
+@Sampled
+fun launchOverlayActivityStackSample() {
+ // Creates an overlay container on the right
+ val params =
+ OverlayCreateParams(
+ overlayAttributes =
+ OverlayAttributes(
+ EmbeddingBounds(
+ alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
+ width = ratio(0.5f),
+ height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+ )
+ )
+ )
+
+ val optionsWithOverlayParams =
+ ActivityOptionsCompat.makeBasic()
+ .toBundle()
+ ?.setOverlayCreateParams(launchingActivity, params)
+
+ // Start INTENT to the overlay container specified by params.
+ launchingActivity.startActivity(INTENT, optionsWithOverlayParams)
+}
+
+@Sampled
+fun overlayAttributesCalculatorSample() {
+ // A sample to show overlay on the bottom if the device is portrait, and on the right when
+ // the device is landscape.
+ OverlayController.getInstance(launchingActivity).setOverlayAttributesCalculator { params ->
+ val taskBounds = params.parentWindowMetrics.bounds
+ return@setOverlayAttributesCalculator OverlayAttributes(
+ if (taskBounds.isPortrait()) {
+ EmbeddingBounds(
+ alignment = EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+ width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+ height = ratio(0.5f),
+ )
+ } else {
+ EmbeddingBounds(
+ alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
+ width = ratio(0.5f),
+ height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+ )
+ }
+ )
+ }
+}
+
+private fun Rect.isPortrait(): Boolean = height() >= width()
+
+val launchingActivity: Activity = Activity()
+
+val INTENT = Intent()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index ecf1f67..7126476 100644
--- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -17,7 +17,10 @@
package androidx.window.samples.embedding
import android.app.Application
+import android.graphics.Color
import androidx.annotation.Sampled
+import androidx.window.embedding.EmbeddingAnimationBackground
+import androidx.window.embedding.EmbeddingAnimationParams
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
@@ -57,6 +60,16 @@
SplitAttributes.LayoutDirection.LOCALE
}
)
+ // Optionally set the animation background and change transition animation to use
+ // when switching between vertical and horizontal
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+ )
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
}
return@setSplitAttributesCalculator if (
@@ -66,6 +79,16 @@
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ // Optionally set the animation background and change transition animation to use
+ // when switching between vertical and horizontal
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+ )
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
} else {
// Expand containers if the device is in portrait or the width is less than 600 dp.
@@ -84,12 +107,30 @@
return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) {
builder
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- // Set the color to use when switching between vertical and horizontal
+ // Optionally set the animation background and change transition animation to use
+ // when switching between vertical and horizontal
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+ )
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
} else if (parentConfiguration.screenHeightDp >= 600) {
builder
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
- // Set the color to use when switching between vertical and horizontal
+ // Optionally set the animation background and change transition animation to use
+ // when switching between vertical and horizontal
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+ )
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
} else {
// Fallback to expand the secondary container
diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
index e0d60f16..2393bc6 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -42,5 +42,13 @@
android:name=
"android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
android:value="false" />
+ <property
+ android:name=
+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+ android:value="false" />
+ <property
+ android:name=
+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
+ android:value="false" />
</application>
</manifest>
diff --git a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
index 1995125..54c87a3 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
@@ -119,6 +119,42 @@
}
}
+ @Test
+ fun test_property_allow_user_aspect_ratio_override() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // No-op, but to suppress lint
+ return
+ }
+ activityRule.scenario.onActivity { activity ->
+ // Should be false as defined in AndroidManifest.xml
+ assertFalse(
+ getProperty(
+ activity,
+ WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE
+ )
+ )
+ }
+ }
+
+ @Test
+ fun test_property_allow_user_aspect_ratio_fullscreen_override() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // No-op, but to suppress lint
+ return
+ }
+ activityRule.scenario.onActivity { activity ->
+ // Should be false as defined in AndroidManifest.xml
+ assertFalse(
+ getProperty(
+ activity,
+ WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE
+ )
+ )
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.S)
@Throws(PackageManager.NameNotFoundException::class)
private fun getProperty(context: Context, propertyName: String): Boolean {
diff --git a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
index ca278ee..6199b64 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
@@ -1,5 +1,6 @@
package androidx.window
+import android.app.Activity
import android.app.Application
import android.content.Context
import android.hardware.display.DisplayManager
@@ -7,7 +8,10 @@
import android.view.Display
import android.view.WindowManager
import androidx.annotation.RequiresApi
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.junit.Assume.assumeTrue
open class WindowTestUtils {
@@ -28,17 +32,59 @@
)
}
- @OptIn(androidx.window.core.ExperimentalWindowApi::class)
fun assumeAtLeastVendorApiLevel(min: Int) {
- val version = WindowSdkExtensions.getInstance().extensionVersion
- assumeTrue(version >= min)
+ val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
+ assumeTrue(apiLevel >= min)
}
- @OptIn(androidx.window.core.ExperimentalWindowApi::class)
fun assumeBeforeVendorApiLevel(max: Int) {
- val version = WindowSdkExtensions.getInstance().extensionVersion
- assumeTrue(version < max)
- assumeTrue(version > 0)
+ val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
+ assumeTrue(apiLevel < max)
+ assumeTrue(apiLevel > 0)
+ }
+
+ fun isInMultiWindowMode(activity: Activity): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ activity.isInMultiWindowMode
+ } else false
+ }
+
+ fun assumePlatformBeforeR() {
+ assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
+ }
+
+ fun assumePlatformROrAbove() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+ }
+
+ fun assumePlatformBeforeU() {
+ assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ }
+
+ fun assumePlatformUOrAbove() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ }
+
+ /**
+ * Creates and launches an activity performing the supplied actions at various points in the
+ * activity lifecycle.
+ *
+ * @param initialAction the action that will run once before the activity is created.
+ * @param verifyAction the action to run once after each change in activity lifecycle state.
+ */
+ fun runActionsAcrossActivityLifecycle(
+ scenarioRule: ActivityScenarioRule<TestActivity>,
+ initialAction: ActivityScenario.ActivityAction<TestActivity>,
+ verifyAction: ActivityScenario.ActivityAction<TestActivity>
+ ) {
+ val scenario = scenarioRule.scenario
+ scenario.onActivity(initialAction)
+ scenario.moveToState(Lifecycle.State.CREATED)
+ scenario.onActivity(verifyAction)
+ scenario.moveToState(Lifecycle.State.STARTED)
+ scenario.onActivity(verifyAction)
+ scenario.moveToState(Lifecycle.State.RESUMED)
+ scenario.onActivity(verifyAction)
}
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
index 3d658dc..76daa1b 100644
--- a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
@@ -18,6 +18,7 @@
import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.WindowExtensionsProvider
+import kotlin.test.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assume.assumeTrue
import org.junit.Test
@@ -35,15 +36,15 @@
*/
@Test
fun windowAreaComponentIsAvailable_ifProviderIsAvailable() {
- assumeTrue(ExtensionsUtil.safeVendorApiLevel >= 2)
+ assumeTrue(ExtensionsUtil.safeVendorApiLevel >= 3)
val loader = SafeWindowAreaComponentProvider::class.java.classLoader!!
val safeComponent = SafeWindowAreaComponentProvider(loader).windowAreaComponent
try {
val extensions = WindowExtensionsProvider.getWindowExtensions()
val actualComponent = extensions.windowAreaComponent
- if (actualComponent == null) {
- assertNull(safeComponent)
+ if (actualComponent != null) {
+ assertNotNull(safeComponent)
}
// TODO(b/267831038): verify upon each api level
// TODO(b/267708462): more reliable test for testing actual method matching
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
index 21ecaa1..514ab50 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -23,6 +23,7 @@
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
+import android.view.Window
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.test.ext.junit.rules.ActivityScenarioRule
@@ -84,11 +85,7 @@
assumeAtLeastVendorApiLevel(minVendorApiLevel)
activityScenario.scenario.onActivity {
val extensionComponent = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(
- windowAreaComponent = extensionComponent,
- vendorApiLevel = FEATURE_VENDOR_API_LEVEL
- )
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
extensionComponent.currentRearDisplayStatus = STATUS_UNAVAILABLE
extensionComponent.currentRearDisplayPresentationStatus = STATUS_UNAVAILABLE
val collector = TestWindowAreaInfoListConsumer()
@@ -168,11 +165,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(
- windowAreaComponent = extensions,
- vendorApiLevel = FEATURE_VENDOR_API_LEVEL
- )
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
extensions.currentRearDisplayStatus = STATUS_AVAILABLE
val callback = TestWindowAreaSessionCallback()
val windowAreaInfo: WindowAreaInfo? =
@@ -243,11 +236,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(
- windowAreaComponent = extensions,
- vendorApiLevel = FEATURE_VENDOR_API_LEVEL
- )
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
extensions.currentRearDisplayStatus = initialState
val callback = TestWindowAreaSessionCallback()
val windowAreaInfo: WindowAreaInfo? =
@@ -291,11 +280,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(
- windowAreaComponent = extensions,
- vendorApiLevel = FEATURE_VENDOR_API_LEVEL
- )
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
extensions.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
@@ -345,8 +330,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
extensions.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
@@ -366,8 +350,7 @@
}
// Create a new controller to start the presentation.
- val controller2 =
- WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+ val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
val callback = TestWindowAreaPresentationSessionCallback()
activityScenario.scenario.onActivity { testActivity ->
@@ -401,8 +384,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensions = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
extensions.currentRearDisplayStatus = STATUS_AVAILABLE
val callback = TestWindowAreaSessionCallback()
val windowAreaInfo =
@@ -426,8 +408,7 @@
}
// Create a new controller to start the transfer.
- val controller2 =
- WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+ val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
activityScenario.scenario.onActivity { testActivity ->
assert(
@@ -464,11 +445,7 @@
testScope.runTest {
assumeAtLeastVendorApiLevel(minVendorApiLevel)
val extensionComponent = FakeWindowAreaComponent()
- val controller =
- WindowAreaControllerImpl(
- windowAreaComponent = extensionComponent,
- vendorApiLevel = FEATURE_VENDOR_API_LEVEL
- )
+ val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
@@ -580,7 +557,7 @@
rearDisplayPresentationSessionConsumer?.accept(SESSION_STATE_INACTIVE)
}
- override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+ override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation {
return TestExtensionWindowAreaPresentation(
testActivity!!,
rearDisplayPresentationSessionConsumer!!
@@ -675,11 +652,13 @@
override fun setPresentationView(view: View) {
sessionConsumer.accept(WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE)
}
+
+ override fun getWindow(): Window {
+ return activity.window
+ }
}
companion object {
private const val REAR_FACING_BINDER_DESCRIPTION = "TEST_WINDOW_AREA_REAR_FACING"
-
- private const val FEATURE_VENDOR_API_LEVEL = 3
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
index 2f3a207..a54dc60a 100644
--- a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -37,33 +37,28 @@
assertTrue(
WindowAreaComponentValidator.isWindowAreaComponentValid(
WindowAreaComponentFullImplementation::class.java,
- 2
- )
- )
- assertTrue(
- WindowAreaComponentValidator.isWindowAreaComponentValid(
- WindowAreaComponentFullImplementation::class.java,
- 3
+ apiLevel = 3
)
)
}
- /**
- * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
- * implementation.
- */
@Test
- fun isWindowAreaComponentValid_apiLevel2() {
- assertTrue(
- WindowAreaComponentValidator.isWindowAreaComponentValid(
- WindowAreaComponentApiV2Implementation::class.java,
- 2
- )
- )
+ fun isWindowAreaComponentValid_apiLevel1() {
assertFalse(
WindowAreaComponentValidator.isWindowAreaComponentValid(
- IncompleteWindowAreaComponentApiV2Implementation::class.java,
- 3
+ WindowAreaComponentApiV3Implementation::class.java,
+ apiLevel = 1
+ )
+ )
+ }
+
+ /** Test that validator returns false for API Level 2. */
+ @Test
+ fun isWindowAreaComponentValid_apiLevel2() {
+ assertFalse(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV3Implementation::class.java,
+ 2
)
)
}
@@ -74,7 +69,7 @@
*/
@Test
fun isWindowAreaComponentValid_apiLevel3() {
- assertTrue(
+ assertFalse(
WindowAreaComponentValidator.isWindowAreaComponentValid(
WindowAreaComponentApiV3Implementation::class.java,
2
@@ -99,10 +94,14 @@
)
}
- /** Test that validator returns true if the [ExtensionWindowAreaStatus] is valid */
+ /**
+ * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid and expected to
+ * be found on that vendorApiLevel. Verifies that true is returned if the vendorApiLevel is a
+ * version before [ExtensionWindowAreaStatus] was introduced.
+ */
@Test
fun isExtensionWindowAreaStatusValid_trueIfValid() {
- assertTrue(
+ assertFalse(
WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
ValidExtensionWindowAreaStatus::class.java,
2
@@ -116,18 +115,15 @@
)
}
- /** Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete */
+ /**
+ * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete and
+ * expected to be on the device.
+ */
@Test
fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
assertFalse(
WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
IncompleteExtensionWindowAreaStatus::class.java,
- 2
- )
- )
- assertFalse(
- WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
- IncompleteExtensionWindowAreaStatus::class.java,
3
)
)
@@ -177,24 +173,6 @@
}
}
- private class WindowAreaComponentApiV2Implementation : WindowAreaComponentApi2Requirements {
- override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
- throw NotImplementedError("Not implemented")
- }
-
- override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
- throw NotImplementedError("Not implemented")
- }
-
- override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
- throw NotImplementedError("Not implemented")
- }
-
- override fun endRearDisplaySession() {
- throw NotImplementedError("Not implemented")
- }
- }
-
private class WindowAreaComponentApiV3Implementation : WindowAreaComponentApi3Requirements {
override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
throw NotImplementedError("Not implemented")
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
index fc53f9e..6e8a2a4 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -17,20 +17,28 @@
package androidx.window.embedding
import android.app.Activity
+import android.content.res.Resources
+import android.graphics.Color
import android.os.Binder
import android.os.IBinder
import androidx.window.WindowSdkExtensions
import androidx.window.WindowTestUtils
import androidx.window.core.PredicateAdapter
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
import androidx.window.embedding.SplitAttributes.SplitType
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.ActivityStack.Token as OEMActivityStackToken
+import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
+import androidx.window.extensions.embedding.AnimationParams as OEMEmbeddingAnimationParams
+import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import androidx.window.extensions.embedding.SplitInfo.Token as OEMSplitInfoToken
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -39,6 +47,7 @@
/** Tests for [EmbeddingAdapter] */
class EmbeddingAdapterTest {
+
private lateinit var adapter: EmbeddingAdapter
private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
@@ -52,51 +61,158 @@
}
@Test
- fun testTranslateSplitInfoWithDefaultAttrs() {
+ fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel2() {
WindowTestUtils.assumeAtLeastVendorApiLevel(2)
WindowTestUtils.assumeBeforeVendorApiLevel(3)
- val oemSplitInfo =
- createTestOEMSplitInfo(
- createTestOEMActivityStack(ArrayList(), true),
- createTestOEMActivityStack(ArrayList(), true),
- OEMSplitAttributes.Builder().build(),
- )
+ val oemSplitInfo = createTestOEMSplitInfo(OEMSplitAttributes.Builder().build())
val expectedSplitInfo =
SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
SplitAttributes.Builder()
.setSplitType(SplitType.SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
.build(),
- INVALID_SPLIT_INFO_TOKEN,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel3() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+ WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ OEMSplitAttributes.Builder().build(),
+ testBinder = Binder(),
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ SplitAttributes.Builder()
+ .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build(),
+ oemSplitInfo.token,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@Test
- fun testTranslateSplitInfoWithExpandingContainers() {
+ fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel5() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ OEMSplitAttributes.Builder().build(),
+ testToken = OEMSplitInfoToken.createFromBinder(Binder()),
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(
+ ArrayList(),
+ isEmpty = true,
+ oemSplitInfo.primaryActivityStack.activityStackToken,
+ ),
+ ActivityStack(
+ ArrayList(),
+ isEmpty = true,
+ oemSplitInfo.secondaryActivityStack.activityStackToken,
+ ),
+ SplitAttributes.Builder()
+ .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build(),
+ oemSplitInfo.splitInfoToken,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Test
+ fun testTranslateSplitInfoWithExpandingContainersWithApiLevel2() {
WindowTestUtils.assumeAtLeastVendorApiLevel(2)
WindowTestUtils.assumeBeforeVendorApiLevel(3)
val oemSplitInfo =
createTestOEMSplitInfo(
- createTestOEMActivityStack(ArrayList(), true),
- createTestOEMActivityStack(ArrayList(), true),
OEMSplitAttributes.Builder()
.setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
- .build(),
+ .build()
)
val expectedSplitInfo =
SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
SplitAttributes.Builder()
.setSplitType(SplitType.SPLIT_TYPE_EXPAND)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
.build(),
- INVALID_SPLIT_INFO_TOKEN,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun testTranslateSplitInfoWithExpandingContainersWithApiLevel3() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+ WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ OEMSplitAttributes.Builder()
+ .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
+ .build(),
+ testBinder = Binder(),
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ SplitAttributes.Builder()
+ .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build(),
+ oemSplitInfo.token,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Test
+ fun testTranslateSplitInfoWithExpandingContainersWithApiLevel5() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ OEMSplitAttributes.Builder()
+ .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
+ .build(),
+ testToken = OEMSplitInfoToken.createFromBinder(Binder()),
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(
+ ArrayList(),
+ isEmpty = true,
+ oemSplitInfo.primaryActivityStack.activityStackToken
+ ),
+ ActivityStack(
+ ArrayList(),
+ isEmpty = true,
+ oemSplitInfo.secondaryActivityStack.activityStackToken
+ ),
+ SplitAttributes.Builder()
+ .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build(),
+ oemSplitInfo.splitInfoToken,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -117,14 +233,13 @@
val expectedSplitInfo =
SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
SplitAttributes.Builder()
.setSplitType(SplitType.ratio(expectedSplitRatio))
// OEMSplitInfo with Vendor API level 1 doesn't provide layoutDirection.
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
.build(),
- INVALID_SPLIT_INFO_TOKEN,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -136,8 +251,6 @@
val oemSplitInfo =
createTestOEMSplitInfo(
- createTestOEMActivityStack(ArrayList(), true),
- createTestOEMActivityStack(ArrayList(), true),
OEMSplitAttributes.Builder()
.setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
.setLayoutDirection(TOP_TO_BOTTOM)
@@ -145,53 +258,425 @@
)
val expectedSplitInfo =
SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
.build(),
- INVALID_SPLIT_INFO_TOKEN,
- )
- assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
- }
-
- @Test
- fun testTranslateSplitInfoWithApiLevel3() {
- WindowTestUtils.assumeAtLeastVendorApiLevel(3)
- WindowTestUtils.assumeBeforeVendorApiLevel(5)
-
- val testStackToken = Binder()
- val testSplitInfoToken = Binder()
- val oemSplitInfo =
- createTestOEMSplitInfo(
- createTestOEMActivityStack(ArrayList(), true, testStackToken),
- createTestOEMActivityStack(ArrayList(), true, testStackToken),
- OEMSplitAttributes.Builder()
- .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
- .setLayoutDirection(TOP_TO_BOTTOM)
- .build(),
- testSplitInfoToken,
- )
- val expectedSplitInfo =
- SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
- SplitAttributes.Builder()
- .setSplitType(SPLIT_TYPE_HINGE)
- .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
- .build(),
- testSplitInfoToken,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@Suppress("DEPRECATION")
+ @Test
+ fun testTranslateSplitInfoWithApiLevel3() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+ WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ OEMSplitAttributes.Builder()
+ .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
+ .setLayoutDirection(TOP_TO_BOTTOM)
+ .build(),
+ testBinder = Binder()
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(ArrayList(), isEmpty = true),
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_HINGE)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+ .build(),
+ oemSplitInfo.token,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Test
+ fun testTranslateSplitInfoWithApiLevel5() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+ val oemSplitInfo =
+ createTestOEMSplitInfo(
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+ OEMSplitAttributes.Builder()
+ .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
+ .setLayoutDirection(TOP_TO_BOTTOM)
+ .build(),
+ testToken = OEMSplitInfoToken.createFromBinder(Binder())
+ )
+ val expectedSplitInfo =
+ SplitInfo(
+ ActivityStack(
+ emptyList(),
+ isEmpty = true,
+ oemSplitInfo.primaryActivityStack.activityStackToken,
+ ),
+ ActivityStack(
+ emptyList(),
+ isEmpty = true,
+ oemSplitInfo.secondaryActivityStack.activityStackToken,
+ ),
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_HINGE)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+ .build(),
+ token = oemSplitInfo.splitInfoToken,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Test
+ fun testTranslateAnimationBackgroundWithApiLevel7() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+
+ val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ val animationParamsWithColorBackground =
+ EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+ val splitAttributesWithColorBackground =
+ SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+ val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+ val splitAttributesWithDefaultBackground =
+ SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+ val extensionsColorBackground =
+ OEMEmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ val extensionAnimationParamsWithColorBackground =
+ OEMEmbeddingAnimationParams.Builder()
+ .setAnimationBackground(extensionsColorBackground)
+ .build()
+ val extensionsSplitAttributesWithColorBackground =
+ OEMSplitAttributes.Builder()
+ .setAnimationParams(extensionAnimationParamsWithColorBackground)
+ .build()
+
+ val extensionAnimationParamsWithDefaultBackground =
+ OEMEmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+ )
+ .build()
+ val extensionsSplitAttributesWithDefaultBackground =
+ OEMSplitAttributes.Builder()
+ .setAnimationParams(extensionAnimationParamsWithDefaultBackground)
+ .build()
+
+ // Translate from Window to Extensions
+ assertEquals(
+ extensionsSplitAttributesWithColorBackground,
+ adapter.translateSplitAttributes(splitAttributesWithColorBackground)
+ )
+ assertEquals(
+ extensionsSplitAttributesWithDefaultBackground,
+ adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+ )
+
+ // Translate from Extensions to Window
+ assertEquals(
+ splitAttributesWithColorBackground,
+ adapter.translate(extensionsSplitAttributesWithColorBackground)
+ )
+ assertEquals(
+ splitAttributesWithDefaultBackground,
+ adapter.translate(extensionsSplitAttributesWithDefaultBackground)
+ )
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun testTranslateAnimationBackgroundWithApiLevel5And6() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+ WindowTestUtils.assumeBeforeVendorApiLevel(7)
+
+ val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ val animationParamsWithColorBackground =
+ EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+ val splitAttributesWithColorBackground =
+ SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+ val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+ val splitAttributesWithDefaultBackground =
+ SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+ val extensionsColorBackground =
+ OEMEmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ val extensionsSplitAttributesWithColorBackground =
+ OEMSplitAttributes.Builder().setAnimationBackground(extensionsColorBackground).build()
+ val extensionsSplitAttributesWithDefaultBackground =
+ OEMSplitAttributes.Builder()
+ .setAnimationBackground(
+ OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+ )
+ .build()
+
+ // Translate from Window to Extensions
+ assertEquals(
+ extensionsSplitAttributesWithColorBackground,
+ adapter.translateSplitAttributes(splitAttributesWithColorBackground)
+ )
+ assertEquals(
+ extensionsSplitAttributesWithDefaultBackground,
+ adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+ )
+
+ // Translate from Extensions to Window
+ assertEquals(
+ splitAttributesWithColorBackground,
+ adapter.translate(extensionsSplitAttributesWithColorBackground)
+ )
+ assertEquals(
+ splitAttributesWithDefaultBackground,
+ adapter.translate(extensionsSplitAttributesWithDefaultBackground)
+ )
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun testTranslateAnimationBackgroundBeforeApiLevel5() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(2)
+ WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+ val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ val animationParamsWithColorBackground =
+ EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+ val splitAttributesWithColorBackground =
+ SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+ val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+ val splitAttributesWithDefaultBackground =
+ SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+ // No difference after translate before API level 5
+ assertEquals(
+ adapter.translateSplitAttributes(splitAttributesWithColorBackground),
+ adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+ )
+ }
+
+ @Test
+ fun testTranslateAnimationSpecWithApiLevel7() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+
+ val animationParamsWithJumpCut =
+ EmbeddingAnimationParams.Builder()
+ .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ val splitAttributesWithJumpCutAnimationParams =
+ SplitAttributes.Builder().setAnimationParams(animationParamsWithJumpCut).build()
+ val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+ val splitAttributesWithDefaultAnimationParams =
+ SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+ val extensionsAnimationParamsWithJumpCut =
+ OEMEmbeddingAnimationParams.Builder()
+ .setOpenAnimationResId(Resources.ID_NULL)
+ .setCloseAnimationResId(Resources.ID_NULL)
+ .setChangeAnimationResId(Resources.ID_NULL)
+ .build()
+ val extensionsSplitAttributesWithJumpCutAnimationParams =
+ OEMSplitAttributes.Builder()
+ .setAnimationParams(extensionsAnimationParamsWithJumpCut)
+ .build()
+ val oemDefaultAnimationParams = OEMEmbeddingAnimationParams.Builder().build()
+ val extensionsSplitAttributesWithDefaultAnimationParams =
+ OEMSplitAttributes.Builder().setAnimationParams(oemDefaultAnimationParams).build()
+
+ // Translate from Window to Extensions
+ assertEquals(
+ extensionsSplitAttributesWithJumpCutAnimationParams,
+ adapter.translateSplitAttributes(splitAttributesWithJumpCutAnimationParams)
+ )
+ assertEquals(
+ extensionsSplitAttributesWithDefaultAnimationParams,
+ adapter.translateSplitAttributes(splitAttributesWithDefaultAnimationParams)
+ )
+
+ // Translate from Extensions to Window
+ assertEquals(
+ splitAttributesWithJumpCutAnimationParams,
+ adapter.translate(extensionsSplitAttributesWithJumpCutAnimationParams)
+ )
+ assertEquals(
+ splitAttributesWithDefaultAnimationParams,
+ adapter.translate(extensionsSplitAttributesWithDefaultAnimationParams)
+ )
+ }
+
+ @Test
+ fun testTranslateAnimationSpecBeforeApiLevel7() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(2)
+ WindowTestUtils.assumeBeforeVendorApiLevel(7)
+
+ val animationParamsWithJumpCut =
+ EmbeddingAnimationParams.Builder()
+ .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ val splitAttributesWithJumpCutAnimationParams =
+ SplitAttributes.Builder().setAnimationParams(animationParamsWithJumpCut).build()
+ val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+ val splitAttributesWithDefaultAnimationParams =
+ SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+ // No difference after translate before API level 7
+ assertEquals(
+ adapter.translateSplitAttributes(splitAttributesWithJumpCutAnimationParams),
+ adapter.translateSplitAttributes(splitAttributesWithDefaultAnimationParams)
+ )
+ }
+
+ @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+ @Test
+ fun testTranslateEmbeddingConfigurationToWindowAttributes() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+ val dimAreaBehavior = EmbeddingConfiguration.DimAreaBehavior.ON_TASK
+ adapter.embeddingConfiguration = EmbeddingConfiguration(dimAreaBehavior)
+ val oemSplitAttributes = adapter.translateSplitAttributes(SplitAttributes.Builder().build())
+
+ assertEquals(dimAreaBehavior.value, oemSplitAttributes.windowAttributes.dimAreaBehavior)
+ }
+
+ @Test
+ fun testTranslateDividerAttributes_draggable() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+ val dividerAttributes =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(20)
+ .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+ .setColor(Color.GRAY)
+ .build()
+ val oemDividerAttributes =
+ OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.7f)
+ .setDividerColor(Color.GRAY)
+ .build()
+
+ assertEquals(
+ oemDividerAttributes,
+ adapter.translateToOemDividerAttributes(dividerAttributes)
+ )
+ assertEquals(
+ dividerAttributes,
+ adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+ )
+ }
+
+ @Test
+ fun testTranslateDividerAttributes_dragToFullscreen() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+ val dividerAttributes =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(20)
+ .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+ .setColor(Color.GRAY)
+ .setDraggingToFullscreenAllowed(true)
+ .build()
+ val oemDividerAttributes =
+ OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.7f)
+ .setDividerColor(Color.GRAY)
+ .setDraggingToFullscreenAllowed(true)
+ .build()
+
+ val dividerAttributes2 =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(20)
+ .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+ .setColor(Color.GRAY)
+ .build()
+ val oemDividerAttributes2 =
+ OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(20)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.7f)
+ .setDividerColor(Color.GRAY)
+ .build()
+
+ assertEquals(
+ oemDividerAttributes,
+ adapter.translateToOemDividerAttributes(dividerAttributes)
+ )
+ assertEquals(
+ dividerAttributes,
+ adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+ )
+ assertEquals(
+ oemDividerAttributes2,
+ adapter.translateToOemDividerAttributes(dividerAttributes2)
+ )
+ assertEquals(
+ dividerAttributes2,
+ adapter.translateToJetpackDividerAttributes(oemDividerAttributes2)
+ )
+ }
+
+ @Test
+ fun testTranslateDividerAttributes_fixed() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+ val dividerAttributes =
+ FixedDividerAttributes.Builder().setWidthDp(20).setColor(Color.GRAY).build()
+ val oemDividerAttributes =
+ OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_FIXED)
+ .setWidthDp(20)
+ .setDividerColor(Color.GRAY)
+ .build()
+
+ assertEquals(
+ oemDividerAttributes,
+ adapter.translateToOemDividerAttributes(dividerAttributes)
+ )
+ assertEquals(
+ dividerAttributes,
+ adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+ )
+ }
+
+ @Test
+ fun testTranslateDividerAttributes_noDivider() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+ val dividerAttributes = DividerAttributes.NO_DIVIDER
+ val oemDividerAttributes = null
+
+ assertEquals(
+ oemDividerAttributes,
+ adapter.translateToOemDividerAttributes(dividerAttributes)
+ )
+ assertEquals(
+ dividerAttributes,
+ adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+ )
+ }
+
+ private fun createTestOEMSplitInfo(
+ testSplitAttributes: OEMSplitAttributes,
+ testBinder: IBinder? = null,
+ testToken: OEMSplitInfo.Token? = null,
+ ): OEMSplitInfo =
+ createTestOEMSplitInfo(
+ createTestOEMActivityStack(),
+ createTestOEMActivityStack(),
+ testSplitAttributes,
+ testBinder,
+ testToken,
+ )
+
+ @Suppress("Deprecation") // Verify the behavior of version 3 and 4.
private fun createTestOEMSplitInfo(
testPrimaryActivityStack: OEMActivityStack,
testSecondaryActivityStack: OEMActivityStack,
testSplitAttributes: OEMSplitAttributes,
- testToken: IBinder = INVALID_SPLIT_INFO_TOKEN,
+ testBinder: IBinder? = null,
+ testToken: OEMSplitInfoToken? = null,
): OEMSplitInfo {
return mock<OEMSplitInfo>().apply {
whenever(primaryActivityStack).thenReturn(testPrimaryActivityStack)
@@ -199,23 +684,32 @@
if (extensionVersion >= 2) {
whenever(splitAttributes).thenReturn(testSplitAttributes)
}
- if (extensionVersion >= 3) {
- whenever(token).thenReturn(testToken)
+ when (extensionVersion) {
+ in 3..4 -> whenever(token).thenReturn(testBinder)
+ in 5..Int.MAX_VALUE -> whenever(splitInfoToken).thenReturn(testToken)
}
}
}
- @Suppress("DEPRECATION")
+ private fun createTestOEMActivityStack(
+ testToken: OEMActivityStackToken? = null
+ ): OEMActivityStack =
+ createTestOEMActivityStack(
+ emptyList(),
+ testIsEmpty = true,
+ testToken,
+ )
+
private fun createTestOEMActivityStack(
testActivities: List<Activity>,
testIsEmpty: Boolean,
- testToken: IBinder = INVALID_ACTIVITY_STACK_TOKEN,
+ testToken: OEMActivityStackToken? = null,
): OEMActivityStack {
return mock<OEMActivityStack>().apply {
whenever(activities).thenReturn(testActivities)
whenever(isEmpty).thenReturn(testIsEmpty)
- if (extensionVersion in 3..4) {
- whenever(token).thenReturn(testToken)
+ if (extensionVersion >= 5) {
+ whenever(activityStackToken).thenReturn(testToken)
}
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
index 7fa2cc6..796862e 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
@@ -19,6 +19,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import androidx.annotation.RequiresApi
@@ -79,6 +80,7 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -136,6 +138,16 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(TOP_TO_BOTTOM)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+ )
+ .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
assertEquals(TEST_TAG, rule.tag)
assertEquals(NEVER, rule.finishPrimaryWithSecondary)
@@ -163,6 +175,7 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -226,6 +239,18 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(BOTTOM_TO_TOP)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(
+ application.resources.getColor(R.color.testColor, null)
+ )
+ )
+ .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
assertEquals(TEST_TAG, rule.tag)
assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
index 447640c..be687ee 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
@@ -17,8 +17,8 @@
package androidx.window.embedding
import android.util.Log
+import androidx.window.WindowSdkExtensions
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.WindowExtensions
import androidx.window.extensions.WindowExtensionsProvider
import org.junit.Assert.assertNotNull
@@ -63,9 +63,13 @@
// TODO(b/267708462) : more reliable test for testing actual method matching
assertNotNull(safeComponent)
assertTrue(safeProvider.isActivityEmbeddingComponentAccessible())
- when (ExtensionsUtil.safeVendorApiLevel) {
+ when (WindowSdkExtensions.getInstance().extensionVersion) {
1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
- else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+ 2 -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+ in 3..4 -> assertTrue(safeProvider.hasValidVendorApiLevel3())
+ 5 -> assertTrue(safeProvider.hasValidVendorApiLevel5())
+ 6 -> assertTrue(safeProvider.hasValidVendorApiLevel6())
+ 7 -> assertTrue(safeProvider.hasValidVendorApiLevel7())
}
}
} catch (e: UnsupportedOperationException) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
index 784a5c5..1c166e87 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
@@ -52,9 +52,14 @@
// TODO(b/267708462): more reliable test for testing actual method matching
assertNotNull(safeComponent)
assertTrue(safeProvider.isWindowLayoutComponentAccessible())
- when (ExtensionsUtil.safeVendorApiLevel) {
- 1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
- else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+ when {
+ ExtensionsUtil.safeVendorApiLevel == 1 -> {
+ assertTrue(safeProvider.hasValidVendorApiLevel1())
+ }
+ ExtensionsUtil.safeVendorApiLevel < 6 -> {
+ assertTrue(safeProvider.hasValidVendorApiLevel2())
+ }
+ else -> assertTrue(safeProvider.hasValidVendorApiLevel6())
}
}
} catch (e: UnsupportedOperationException) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
index e1a57bc..4560d2f 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
@@ -22,10 +22,13 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.window.TestActivity
import androidx.window.TestConsumer
+import androidx.window.WindowSdkExtensions
import androidx.window.WindowTestUtils
import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
import androidx.window.layout.adapter.WindowBackend
import java.util.concurrent.Executor
+import kotlin.test.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -34,6 +37,7 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
@@ -45,6 +49,7 @@
ActivityScenarioRule(TestActivity::class.java)
private val testScope = TestScope(UnconfinedTestDispatcher())
+ private val windowSdkExtensions = WindowSdkExtensions.getInstance()
init {
Dispatchers.setMain(UnconfinedTestDispatcher())
@@ -54,9 +59,10 @@
public fun testWindowLayoutFeatures(): Unit =
testScope.runTest {
activityScenario.scenario.onActivity { testActivity ->
- val windowMetricsCalculator = WindowMetricsCalculatorCompat
+ val windowMetricsCalculator = WindowMetricsCalculatorCompat()
val fakeBackend = FakeWindowBackend()
- val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+ val repo =
+ WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
val collector = TestConsumer<WindowLayoutInfo>()
testScope.launch(Job()) {
repo.windowLayoutInfo(testActivity).collect(collector::accept)
@@ -74,7 +80,12 @@
}
assumeAtLeastVendorApiLevel(2)
val fakeBackend = FakeWindowBackend()
- val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, fakeBackend)
+ val repo =
+ WindowInfoTrackerImpl(
+ WindowMetricsCalculatorCompat(),
+ fakeBackend,
+ windowSdkExtensions
+ )
val collector = TestConsumer<WindowLayoutInfo>()
val windowContext = WindowTestUtils.createOverlayWindowContext()
@@ -89,9 +100,10 @@
public fun testWindowLayoutFeatures_multicasting(): Unit =
testScope.runTest {
activityScenario.scenario.onActivity { testActivity ->
- val windowMetricsCalculator = WindowMetricsCalculatorCompat
+ val windowMetricsCalculator = WindowMetricsCalculatorCompat()
val fakeBackend = FakeWindowBackend()
- val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+ val repo =
+ WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
val collector = TestConsumer<WindowLayoutInfo>()
val job = Job()
launch(job) { repo.windowLayoutInfo(testActivity).collect(collector::accept) }
@@ -102,15 +114,43 @@
}
@Test
+ fun testSupportedWindowPostures_throwsBeforeApi6() {
+ assumeBeforeVendorApiLevel(6)
+ activityScenario.scenario.onActivity { _ ->
+ val windowMetricsCalculator = WindowMetricsCalculatorCompat()
+ val fakeBackend = FakeWindowBackend()
+ val repo =
+ WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
+ assertThrows(UnsupportedOperationException::class.java) { repo.supportedPostures }
+ }
+ }
+
+ @Test
+ fun testSupportedWindowPostures_reportsFeatures() {
+ assumeAtLeastVendorApiLevel(6)
+ activityScenario.scenario.onActivity { _ ->
+ val windowMetricsCalculator = WindowMetricsCalculatorCompat()
+ val expected = listOf(SupportedPosture.TABLETOP)
+ val fakeBackend = FakeWindowBackend(supportedPostures = expected)
+ val repo =
+ WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
+ val actual = repo.supportedPostures
+
+ assertEquals(expected, actual)
+ }
+ }
+
+ @Test
public fun testWindowLayoutFeatures_multicastingWithContext(): Unit =
testScope.runTest {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return@runTest
}
assumeAtLeastVendorApiLevel(2)
- val windowMetricsCalculator = WindowMetricsCalculatorCompat
+ val windowMetricsCalculator = WindowMetricsCalculatorCompat()
val fakeBackend = FakeWindowBackend()
- val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+ val repo =
+ WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
val collector = TestConsumer<WindowLayoutInfo>()
val job = Job()
@@ -123,7 +163,9 @@
collector.assertValues(WindowLayoutInfo(emptyList()), WindowLayoutInfo(emptyList()))
}
- private class FakeWindowBackend : WindowBackend {
+ private class FakeWindowBackend(
+ override val supportedPostures: List<SupportedPosture> = emptyList()
+ ) : WindowBackend {
private class CallbackHolder(
val executor: Executor,
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
index 16caeae..4096e1e 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
@@ -16,19 +16,24 @@
package androidx.window.layout
import android.annotation.SuppressLint
-import android.app.Activity
import android.content.ContextWrapper
import android.os.Build
import android.view.Display
import android.view.WindowManager
+import androidx.annotation.RequiresApi
import androidx.core.view.WindowInsetsCompat
-import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario.ActivityAction
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeR
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
+import androidx.window.WindowTestUtils.Companion.isInMultiWindowMode
+import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
import androidx.window.core.ExperimentalWindowApi
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assume
@@ -124,8 +129,9 @@
@Test
fun testGetCurrentWindowBounds_postR() {
assumePlatformROrAbove()
- runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
- val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val bounds =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
val windowMetricsBounds = activity.windowManager.currentWindowMetrics.bounds
assertEquals(windowMetricsBounds, bounds)
}
@@ -199,8 +205,9 @@
@Test
fun testGetMaximumWindowBounds_postR() {
assumePlatformROrAbove()
- runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
- val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val bounds =
+ WindowMetricsCalculatorCompat().computeMaximumWindowMetrics(activity).bounds
val windowMetricsBounds = activity.windowManager.maximumWindowMetrics.bounds
assertEquals(windowMetricsBounds, bounds)
}
@@ -211,8 +218,9 @@
@OptIn(ExperimentalWindowApi::class)
fun testGetWindowInsetsCompat_currentWindowMetrics_postR() {
assumePlatformROrAbove()
- runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
- val windowMetrics = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity)
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
val windowInsets = windowMetrics.getWindowInsets()
val platformInsets = activity.windowManager.currentWindowMetrics.windowInsets
val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
@@ -225,8 +233,9 @@
@OptIn(ExperimentalWindowApi::class)
fun testGetWindowInsetsCompat_maximumWindowMetrics_postR() {
assumePlatformROrAbove()
- runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
- val windowMetrics = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity)
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeMaximumWindowMetrics(activity)
val windowInsets = windowMetrics.getWindowInsets()
val platformInsets = activity.windowManager.maximumWindowMetrics.windowInsets
val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
@@ -234,12 +243,41 @@
}
}
+ @Test
+ fun testDensityMatchesDisplayMetricsDensity() {
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val calculator = WindowMetricsCalculatorCompat()
+ val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
+ val maxWindowMetrics = calculator.computeMaximumWindowMetrics(activity)
+ assertEquals(activity.resources.displayMetrics.density, windowMetrics.density)
+ assertEquals(windowMetrics.density, maxWindowMetrics.density)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testConvertedWindowMetricsMatchesPlatformWindowMetrics() {
+ assumePlatformUOrAbove()
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val calculator = WindowMetricsCalculatorCompat()
+ val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
+ val wm = activity.getSystemService(WindowManager::class.java)
+ val androidWindowMetrics = wm.currentWindowMetrics
+ assertEquals(androidWindowMetrics.bounds, windowMetrics.bounds)
+ assertEquals(androidWindowMetrics.density, windowMetrics.density)
+ }
+ }
+
private fun testGetCurrentWindowBoundsMatchesRealDisplaySize(
initialAction: ActivityAction<TestActivity>
) {
val assertWindowBoundsMatchesDisplayAction: ActivityAction<TestActivity> =
AssertCurrentWindowBoundsEqualsRealDisplaySizeAction()
- runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
+ runActionsAcrossActivityLifecycle(
+ activityScenarioRule,
+ initialAction,
+ assertWindowBoundsMatchesDisplayAction
+ )
}
private fun testGetMaximumWindowBoundsMatchesRealDisplaySize(
@@ -247,28 +285,11 @@
) {
val assertWindowBoundsMatchesDisplayAction: ActivityAction<TestActivity> =
AssertMaximumWindowBoundsEqualsRealDisplaySizeAction()
- runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
- }
-
- /**
- * Creates and launches an activity performing the supplied actions at various points in the
- * activity lifecycle.
- *
- * @param initialAction the action that will run once before the activity is created.
- * @param verifyAction the action to run once after each change in activity lifecycle state.
- */
- private fun runActionsAcrossActivityLifecycle(
- initialAction: ActivityAction<TestActivity>,
- verifyAction: ActivityAction<TestActivity>
- ) {
- val scenario = activityScenarioRule.scenario
- scenario.onActivity(initialAction)
- scenario.moveToState(Lifecycle.State.CREATED)
- scenario.onActivity(verifyAction)
- scenario.moveToState(Lifecycle.State.STARTED)
- scenario.onActivity(verifyAction)
- scenario.moveToState(Lifecycle.State.RESUMED)
- scenario.onActivity(verifyAction)
+ runActionsAcrossActivityLifecycle(
+ activityScenarioRule,
+ initialAction,
+ assertWindowBoundsMatchesDisplayAction
+ )
}
private fun assumeNotMultiWindow() {
@@ -295,8 +316,9 @@
} else {
@Suppress("DEPRECATION") activity.windowManager.defaultDisplay
}
- val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
- val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+ val calculator = WindowMetricsCalculatorCompat()
+ val realDisplaySize = getRealSizeForDisplay(display)
+ val bounds = calculator.computeCurrentWindowMetrics(activity).bounds
assertNotEquals("Device can not have zero width", 0, realDisplaySize.x.toLong())
assertNotEquals("Device can not have zero height", 0, realDisplaySize.y.toLong())
assertEquals(
@@ -321,8 +343,9 @@
} else {
@Suppress("DEPRECATION") activity.windowManager.defaultDisplay
}
- val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
- val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
+ val calculator = WindowMetricsCalculatorCompat()
+ val realDisplaySize = getRealSizeForDisplay(display)
+ val bounds = calculator.computeMaximumWindowMetrics(activity).bounds
assertEquals(
"Window bounds width does not match real display width",
realDisplaySize.x.toLong(),
@@ -335,20 +358,4 @@
)
}
}
-
- private companion object {
- private fun isInMultiWindowMode(activity: Activity): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- activity.isInMultiWindowMode
- } else false
- }
-
- private fun assumePlatformBeforeR() {
- Assume.assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
- }
-
- private fun assumePlatformROrAbove() {
- Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
- }
- }
}
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
index fbad2f9..3add8d9 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
@@ -34,81 +34,89 @@
@RunWith(AndroidJUnit4::class)
public class WindowMetricsTest {
@Test
- public fun testGetBounds() {
+ fun testGetBounds() {
val bounds = Rect(1, 2, 3, 4)
- val windowMetrics = WindowMetrics(bounds)
+ val windowMetrics = WindowMetrics(bounds, density = 1f)
assertEquals(bounds, windowMetrics.bounds)
}
@Test
- public fun testEquals_sameBounds() {
+ fun testEquals_sameBounds() {
val bounds = Rect(1, 2, 3, 4)
- val windowMetrics0 = WindowMetrics(bounds)
- val windowMetrics1 = WindowMetrics(bounds)
+ val windowMetrics0 = WindowMetrics(bounds, density = 1f)
+ val windowMetrics1 = WindowMetrics(bounds, density = 1f)
assertEquals(windowMetrics0, windowMetrics1)
}
@Test
- public fun testEquals_differentBounds() {
+ fun testEquals_differentBounds() {
val bounds0 = Rect(1, 2, 3, 4)
- val windowMetrics0 = WindowMetrics(bounds0)
+ val windowMetrics0 = WindowMetrics(bounds0, density = 1f)
val bounds1 = Rect(6, 7, 8, 9)
- val windowMetrics1 = WindowMetrics(bounds1)
+ val windowMetrics1 = WindowMetrics(bounds1, density = 1f)
assertNotEquals(windowMetrics0, windowMetrics1)
}
@Test
- public fun testHashCode_matchesIfEqual() {
+ fun testEquals_differentDensities() {
val bounds = Rect(1, 2, 3, 4)
- val windowMetrics0 = WindowMetrics(bounds)
- val windowMetrics1 = WindowMetrics(bounds)
+ val windowMetrics0 = WindowMetrics(bounds, density = 0f)
+ val windowMetrics1 = WindowMetrics(bounds, density = 1f)
+ assertNotEquals(windowMetrics0, windowMetrics1)
+ }
+
+ @Test
+ fun testHashCode_matchesIfEqual() {
+ val bounds = Rect(1, 2, 3, 4)
+ val windowMetrics0 = WindowMetrics(bounds, density = 1f)
+ val windowMetrics1 = WindowMetrics(bounds, density = 1f)
assertEquals(windowMetrics0.hashCode().toLong(), windowMetrics1.hashCode().toLong())
}
@RequiresApi(Build.VERSION_CODES.R)
@Test
- public fun testSameWindowInsets_emptyInsets() {
+ fun testSameWindowInsets_emptyInsets() {
assumePlatformROrAbove()
val bounds = Bounds(1, 2, 3, 4)
val windowInsetsCompat = WindowInsetsCompat.Builder().build()
- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
assertEquals(windowMetricsA, windowMetricsB)
}
@RequiresApi(Build.VERSION_CODES.R)
@Test
- public fun testSameWindowInsets_nonEmptyInsets() {
+ fun testSameWindowInsets_nonEmptyInsets() {
assumePlatformROrAbove()
val bounds = Bounds(1, 2, 3, 4)
val insets = Insets.of(6, 7, 8, 9)
val builder = WindowInsetsCompat.Builder()
- for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
+ for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
builder.setInsets(type, insets)
}
val windowInsetsCompat = builder.build()
- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
assertEquals(windowMetricsA, windowMetricsB)
}
@RequiresApi(Build.VERSION_CODES.R)
@Test
- public fun testDiffWindowInsets() {
+ fun testDiffWindowInsets() {
assumePlatformROrAbove()
val bounds = Bounds(1, 2, 3, 4)
val insetsA = Insets.of(1, 2, 3, 4)
val insetsB = Insets.of(6, 7, 8, 9)
val builderA = WindowInsetsCompat.Builder()
val builderB = WindowInsetsCompat.Builder()
- for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
+ for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
builderA.setInsets(type, insetsA)
builderB.setInsets(type, insetsB)
}
val windowInsetsCompatA = builderA.build()
val windowInsetsCompatB = builderB.build()
- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA)
- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB)
+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA, 1f /* density */)
+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB, 1f /* density */)
assertNotEquals(windowMetricsA, windowMetricsB)
}
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
index f20e341..c557551 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
@@ -33,17 +33,21 @@
import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.core.util.function.Consumer as OEMConsumer
+import androidx.window.extensions.layout.DisplayFoldFeature
import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
+import androidx.window.extensions.layout.SupportedWindowFeatures
import androidx.window.extensions.layout.WindowLayoutComponent
import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculatorCompat
import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter.translate
import java.util.function.Consumer as JavaConsumer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -645,9 +649,48 @@
consumer.assertValues(expected)
}
+ @Test
+ fun testSupportedFeatures_throwsBeforeApi6() {
+ assumeBeforeVendorApiLevel(6)
+
+ val component = FakeWindowComponent()
+ val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+ assertThrows(UnsupportedOperationException::class.java) { backend.supportedPostures }
+ }
+
+ @Test
+ fun testSupportedFeatures_emptyListReturnsNoFeatures() {
+ assumeAtLeastVendorApiLevel(6)
+
+ val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf()).build()
+ val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
+ val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+ val actual = backend.supportedPostures
+ assertEquals(emptyList<SupportedPosture>(), actual)
+ }
+
+ @Test
+ fun testSupportedFeatures_halfOpenedReturnsTabletopSupport() {
+ assumeAtLeastVendorApiLevel(6)
+
+ val foldFeature =
+ DisplayFoldFeature.Builder(DisplayFoldFeature.TYPE_SCREEN_FOLD_IN)
+ .addProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+ .build()
+ val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf(foldFeature)).build()
+ val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
+ val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+ val actual = backend.supportedPostures
+ assertEquals(listOf(SupportedPosture.TABLETOP), actual)
+ }
+
internal companion object {
private fun newTestOEMWindowLayoutInfo(activity: Activity): OEMWindowLayoutInfo {
- val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+ val bounds =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
val displayFeatures = listOf(feature)
@@ -661,7 +704,7 @@
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun newTestOEMWindowLayoutInfo(@UiContext context: Context): OEMWindowLayoutInfo {
- val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(context).bounds
+ val bounds = WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(context).bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
val displayFeatures = listOf(feature)
@@ -696,7 +739,8 @@
}
}
- private class FakeWindowComponent : WindowLayoutComponent {
+ private class FakeWindowComponent(private val windowFeatures: SupportedWindowFeatures? = null) :
+ WindowLayoutComponent {
val consumers = mutableListOf<JavaConsumer<OEMWindowLayoutInfo>>()
val oemConsumers = mutableListOf<OEMConsumer<OEMWindowLayoutInfo>>()
@@ -723,6 +767,14 @@
oemConsumers.remove(consumer)
}
+ override fun getSupportedWindowFeatures(): SupportedWindowFeatures {
+ return windowFeatures
+ ?: throw UnsupportedOperationException(
+ "Window features are not set. Either the vendor API level is too low or value " +
+ "was not set"
+ )
+ }
+
@SuppressLint("NewApi")
fun emit(info: OEMWindowLayoutInfo) {
if (ExtensionsUtil.safeVendorApiLevel < 2) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
index 3a948d3..325960a 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
@@ -32,7 +32,7 @@
import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
import androidx.window.layout.TestFoldingFeatureUtil.invalidNonZeroFoldBounds
import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
+import androidx.window.layout.WindowMetricsCalculatorCompat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
@@ -50,7 +50,8 @@
@Test
fun testTranslate_foldingFeature() {
activityScenario.scenario.onActivity { activity ->
- val windowMetrics = computeCurrentWindowMetrics(activity)
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
val bounds = windowMetrics.bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
@@ -66,7 +67,8 @@
@Test
fun testTranslate_windowLayoutInfo() {
activityScenario.scenario.onActivity { activity ->
- val bounds = computeCurrentWindowMetrics(activity).bounds
+ val bounds =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
@@ -84,7 +86,8 @@
fun testTranslate_windowLayoutInfoFromContext() {
assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
activityScenario.scenario.onActivity { activity ->
- val bounds = computeCurrentWindowMetrics(activity).bounds
+ val bounds =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
@@ -101,7 +104,8 @@
@Test
fun testTranslate_foldingFeature_invalidType() {
activityScenario.scenario.onActivity { activity ->
- val windowMetrics = computeCurrentWindowMetrics(activity)
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
val bounds = windowMetrics.bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val oemFeature = OEMFoldingFeature(featureBounds, -1, STATE_HALF_OPENED)
@@ -115,7 +119,8 @@
@Test
fun testTranslate_foldingFeature_invalidState() {
activityScenario.scenario.onActivity { activity ->
- val windowMetrics = computeCurrentWindowMetrics(activity)
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
val bounds = windowMetrics.bounds
val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, -1)
@@ -129,7 +134,8 @@
@Test
fun testTranslate_foldingFeature_invalidBounds() {
activityScenario.scenario.onActivity { activity ->
- val windowMetrics = computeCurrentWindowMetrics(activity)
+ val windowMetrics =
+ WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
val windowBounds = windowMetrics.bounds
val source =
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
index 292197c..1936828 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
@@ -42,6 +42,7 @@
import org.junit.Assert.assertNotNull
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatcher
@@ -85,6 +86,7 @@
}
}
+ @Ignore("b/241174887")
@Test
fun testWindowLayoutCallbackOnConfigChange() {
val testScope = TestScope(UnconfinedTestDispatcher())
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
index 69474bd..df07ffc 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
@@ -198,6 +198,13 @@
secondConsumer.assertValues(secondExpected)
}
+ @Test(expected = UnsupportedOperationException::class)
+ fun testSupportedWindowFeatures_throws() {
+ val interfaceCompat = SwitchOnUnregisterExtensionInterfaceCompat()
+ val backend = SidecarWindowBackend(interfaceCompat)
+ backend.supportedPostures
+ }
+
internal companion object {
private fun newTestWindowLayoutInfo(): WindowLayoutInfo {
val feature1: DisplayFeature = HardwareFoldingFeature(Bounds(0, 2, 3, 4), HINGE, FLAT)
diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
new file mode 100644
index 0000000..00d5417
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.os.Build
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [BoundsHelper]. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class BoundsHelperTest {
+
+ @get:Rule
+ var activityScenarioRule: ActivityScenarioRule<TestActivity> =
+ ActivityScenarioRule(TestActivity::class.java)
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ @Test
+ fun testCurrentWindowBounds_postR() {
+ assumePlatformROrAbove()
+ activityScenarioRule.scenario.onActivity { activity ->
+ val currentBounds = BoundsHelper.getInstance().currentWindowBounds(activity)
+ val expectedBounds =
+ activity.getSystemService(WindowManager::class.java).currentWindowMetrics.bounds
+ assertEquals(expectedBounds, currentBounds)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ @Test
+ fun testMaximumWindowBounds_postR() {
+ assumePlatformROrAbove()
+ activityScenarioRule.scenario.onActivity { activity ->
+ val currentBounds = BoundsHelper.getInstance().maximumWindowBounds(activity)
+ val expectedBounds =
+ activity.getSystemService(WindowManager::class.java).maximumWindowMetrics.bounds
+ assertEquals(expectedBounds, currentBounds)
+ }
+ }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
new file mode 100644
index 0000000..19c568a
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.graphics.Rect
+import android.os.Build
+import android.util.DisplayMetrics
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeU
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
+import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for the [DensityCompatHelper] class. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class DensityCompatHelperTest {
+
+ @get:Rule
+ var activityScenarioRule: ActivityScenarioRule<TestActivity> =
+ ActivityScenarioRule(TestActivity::class.java)
+
+ @Test
+ fun testDensityFromContext_beforeU() {
+ assumePlatformBeforeU()
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val helper = DensityCompatHelper.getInstance()
+ assertEquals(activity.resources.displayMetrics.density, helper.density(activity))
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testDensityFromContext_UOrAbove() {
+ assumePlatformUOrAbove()
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val wm = activity.getSystemService(WindowManager::class.java)
+ val helper = DensityCompatHelper.getInstance()
+ assertEquals(wm.currentWindowMetrics.density, helper.density(activity))
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testDensityFromConfiguration_beforeU() {
+ assumePlatformBeforeU()
+ assumePlatformROrAbove()
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val helper = DensityCompatHelper.getInstance()
+
+ @Suppress("DEPRECATION")
+ val fakeWindowMetrics =
+ AndroidWindowMetrics(
+ Rect(0, 0, 0, 0),
+ WindowInsets.Builder().build(),
+ )
+
+ val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
+ val expectedDensity =
+ activity.resources.configuration.densityDpi.toFloat() /
+ DisplayMetrics.DENSITY_DEFAULT
+ assertEquals(expectedDensity, density)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testDensityFromWindowMetrics_UOrAbove() {
+ assumePlatformUOrAbove()
+ runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+ val helper = DensityCompatHelper.getInstance()
+
+ val fakeDensity = 123.456f
+ val fakeWindowMetrics =
+ AndroidWindowMetrics(Rect(0, 0, 0, 0), WindowInsets.Builder().build(), fakeDensity)
+
+ val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
+ assertEquals(fakeDensity, density)
+ }
+ }
+}
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
index 5e75a12..8778c51 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
@@ -23,7 +23,10 @@
window:splitMinHeightDp="600"
window:splitMinSmallestWidthDp="0"
window:splitLayoutDirection="topToBottom"
- window:animationBackgroundColor="#0000FF">
+ window:animationBackgroundColor="#0000FF"
+ window:splitOpenAnimation="jumpCut"
+ window:splitCloseAnimation="jumpCut"
+ window:splitChangeAnimation="jumpCut">
<SplitPairFilter
window:primaryActivityName="A"
window:secondaryActivityName="B"/>
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
index df0871f..ac79a56 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
@@ -24,7 +24,10 @@
window:splitMinHeightDp="600"
window:splitMinSmallestWidthDp="0"
window:splitLayoutDirection="bottomToTop"
- window:animationBackgroundColor="@color/testColor">
+ window:animationBackgroundColor="@color/testColor"
+ window:splitOpenAnimation="jumpCut"
+ window:splitCloseAnimation="jumpCut"
+ window:splitChangeAnimation="jumpCut">
<ActivityFilter
window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
</SplitPlaceholderRule>
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index ed346b6..ae9ce65 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -109,7 +109,6 @@
* </application>
* ```
*/
- // TODO(b/274924641): Make property public
const val PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED =
"android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"
@@ -170,4 +169,88 @@
*/
const val PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES =
"android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
+
+ /**
+ * Application-level [PackageManager][android.content.pm.PackageManager.Property] tag that (when
+ * set to false) informs the system the app has opted out of the user-facing aspect ratio
+ * compatibility override.
+ *
+ * The compatibility override enables device users to set the app's aspect ratio or force the
+ * app to fill the display regardless of the aspect ratio or orientation specified in the app
+ * manifest.
+ *
+ * The aspect ratio compatibility override is exposed to users in device settings. A menu in
+ * device settings lists all apps that have not opted out of the compatibility override. Users
+ * select apps from the menu and set the app aspect ratio on a per-app basis. Typically, the
+ * menu is available only on large screen devices.
+ *
+ * When users apply the aspect ratio override, the minimum aspect ratio specified in the app
+ * manifest is overridden. If users choose a full-screen aspect ratio, the orientation of the
+ * activity is forced to
+ * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER]; see
+ * [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE] to disable the full-screen
+ * option only.
+ *
+ * The user override is intended to improve the app experience on devices that have the ignore
+ * orientation request display setting enabled by OEMs (enables compatibility mode for fixed
+ * orientation on Android 12 (API level 31) or higher; see
+ * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
+ * for more details).
+ *
+ * To opt out of the user aspect ratio compatibility override, add this property to your app
+ * manifest and set the value to `false`. Your app will be excluded from the list of apps in
+ * device settings, and users will not be able to override the app's aspect ratio.
+ *
+ * Not setting this property at all, or setting this property to `true` has no effect.
+ *
+ * **Syntax:**
+ *
+ * ```
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+ * android:value="false" />
+ * </application>
+ * ```
+ */
+ const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE =
+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+
+ /**
+ * Application-level [PackageManager][android.content.pm.PackageManager.Property] tag that (when
+ * set to false) informs the system the app has opted out of the full-screen option of the user
+ * aspect ratio compatibility override settings. (For background information about the user
+ * aspect ratio compatibility override, see [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE].)
+ *
+ * When users apply the full-screen compatibility override, the orientation of the activity is
+ * forced to [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER].
+ *
+ * The user override aims to improve the app experience on devices that have the ignore
+ * orientation request display setting enabled by OEMs (enables compatibility mode for fixed
+ * orientation on Android 12 (API level 31) or higher; see
+ * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
+ * for more details).
+ *
+ * To opt out of the full-screen option of the user aspect ratio compatibility override, add
+ * this property to your app manifest and set the value to `false`. Your app will have
+ * full-screen option removed from the list of user aspect ratio override options in device
+ * settings, and users will not be able to apply full-screen override to your app.
+ *
+ * **Note:** If [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE] is `false`, this property has
+ * no effect.
+ *
+ * Not setting this property at all, or setting this property to `true` has no effect.
+ *
+ * **Syntax:**
+ *
+ * ```
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
+ * android:value="false" />
+ * </application>
+ * ```
+ */
+ const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE =
+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
}
diff --git a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
index 97fd83d..a78df6b 100644
--- a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
+++ b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
@@ -17,6 +17,7 @@
package androidx.window
import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
import androidx.window.core.ExtensionsUtil
/**
@@ -31,7 +32,7 @@
*
* @sample androidx.window.samples.checkWindowSdkExtensionsVersion
*/
-abstract class WindowSdkExtensions internal constructor() {
+abstract class WindowSdkExtensions @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor() {
/**
* Reports the device's extension version
@@ -56,6 +57,25 @@
}
}
+ /**
+ * Checks the [extensionVersion] and throws [UnsupportedOperationException] if the version is
+ * not in the [range].
+ *
+ * This is useful to provide compatibility for APIs updated in 2+ but deprecated in latest
+ * version.
+ *
+ * @param range the required extension range of the targeting API.
+ * @throws UnsupportedOperationException if the required [range] is not satisfied.
+ */
+ internal fun requireExtensionVersion(range: kotlin.ranges.IntRange) {
+ if (extensionVersion !in range) {
+ throw UnsupportedOperationException(
+ "This API requires extension version " +
+ "$range, but the device is on $extensionVersion"
+ )
+ }
+ }
+
companion object {
/** Returns a [WindowSdkExtensions] instance. */
@JvmStatic
@@ -65,17 +85,20 @@
private var decorator: WindowSdkExtensionsDecorator = EmptyDecoratorWindowSdk
- internal fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
decorator = overridingDecorator
}
- internal fun reset() {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ fun reset() {
decorator = EmptyDecoratorWindowSdk
}
}
}
-internal interface WindowSdkExtensionsDecorator {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface WindowSdkExtensionsDecorator {
/** Returns a [WindowSdkExtensions] instance. */
fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions
}
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
index e0e9574..8734411 100644
--- a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
@@ -18,6 +18,8 @@
import android.content.Context
import android.view.View
+import android.view.Window
+import androidx.window.area.utils.PresentationWindowCompatUtils
import androidx.window.core.ExperimentalWindowApi
import androidx.window.extensions.area.ExtensionWindowAreaPresentation
import androidx.window.extensions.area.WindowAreaComponent
@@ -25,11 +27,16 @@
@ExperimentalWindowApi
internal class RearDisplayPresentationSessionPresenterImpl(
private val windowAreaComponent: WindowAreaComponent,
- private val presentation: ExtensionWindowAreaPresentation
+ private val presentation: ExtensionWindowAreaPresentation,
+ vendorApiLevel: Int
) : WindowAreaSessionPresenter {
override val context: Context = presentation.presentationContext
+ override val window: Window? =
+ if (vendorApiLevel >= 4) presentation.window
+ else PresentationWindowCompatUtils.getWindowBeforeVendorApiLevel4(presentation)
+
override fun setContentView(view: View) {
presentation.setPresentationView(view)
}
diff --git a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
index 84b52d0..7ea1d32 100644
--- a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
@@ -15,6 +15,7 @@
*/
package androidx.window.area
+import android.os.Build
import androidx.window.SafeWindowExtensionsProvider
import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid
import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaStatusValid
@@ -41,6 +42,7 @@
if (
windowExtensions != null &&
isWindowAreaProviderValid(windowExtensions) &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
isWindowAreaComponentValid(
windowAreaComponentClass,
ExtensionsUtil.safeVendorApiLevel
@@ -49,11 +51,16 @@
extensionWindowAreaStatusClass,
ExtensionsUtil.safeVendorApiLevel
) &&
- isValidExtensionWindowPresentation()
- )
+ isExtensionWindowAreaPresentationValid(
+ extensionWindowAreaPresentationClass,
+ ExtensionsUtil.safeVendorApiLevel
+ )
+ ) {
windowExtensions.windowAreaComponent
- else null
- } catch (e: Exception) {
+ } else {
+ null
+ }
+ } catch (e: Throwable) {
null
}
}
@@ -67,15 +74,6 @@
}
}
- private fun isValidExtensionWindowPresentation(): Boolean {
- // Not required for API Level 2 or below
- return ExtensionsUtil.safeVendorApiLevel <= 2 ||
- isExtensionWindowAreaPresentationValid(
- extensionWindowAreaPresentationClass,
- ExtensionsUtil.safeVendorApiLevel
- )
- }
-
private val windowAreaComponentClass: Class<*>
get() {
return loader.loadClass(WindowExtensionsConstants.WINDOW_AREA_COMPONENT_CLASS)
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
index 90c9759..6d85d20 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -23,7 +23,6 @@
import androidx.annotation.RestrictTo
import androidx.window.WindowSdkExtensions
import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
-import androidx.window.area.utils.DeviceUtils
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
@@ -147,17 +146,16 @@
}
null
}
+
val deviceSupported =
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
windowAreaComponentExtensions != null &&
- (ExtensionsUtil.safeVendorApiLevel >= 3 ||
- DeviceUtils.hasDeviceMetrics(Build.MANUFACTURER, Build.MODEL))
+ ExtensionsUtil.safeVendorApiLevel >= 3
val controller =
if (deviceSupported) {
WindowAreaControllerImpl(
- windowAreaComponentExtensions!!,
- ExtensionsUtil.safeVendorApiLevel
+ windowAreaComponent = windowAreaComponentExtensions!!,
)
} else {
EmptyWindowAreaControllerImpl()
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
index c8396a0..121e608 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -21,14 +21,15 @@
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.window.RequiresWindowSdkExtension
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNKNOWN
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
import androidx.window.area.adapter.WindowAreaAdapter
-import androidx.window.area.utils.DeviceUtils
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
import androidx.window.extensions.area.ExtensionWindowAreaStatus
import androidx.window.extensions.area.WindowAreaComponent
@@ -57,10 +58,10 @@
* functionality.
*/
@ExperimentalWindowApi
+@RequiresWindowSdkExtension(3)
@RequiresApi(Build.VERSION_CODES.Q)
internal class WindowAreaControllerImpl(
private val windowAreaComponent: WindowAreaComponent,
- private val vendorApiLevel: Int
) : WindowAreaController {
private lateinit var rearDisplaySessionConsumer: Consumer2<Int>
@@ -89,40 +90,24 @@
}
windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
- if (vendorApiLevel > 2) {
- windowAreaComponent.addRearDisplayPresentationStatusListener(
- rearDisplayPresentationListener
- )
- }
+ windowAreaComponent.addRearDisplayPresentationStatusListener(
+ rearDisplayPresentationListener
+ )
awaitClose {
windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
- if (vendorApiLevel > 2) {
- windowAreaComponent.removeRearDisplayPresentationStatusListener(
- rearDisplayPresentationListener
- )
- }
+ windowAreaComponent.removeRearDisplayPresentationStatusListener(
+ rearDisplayPresentationListener
+ )
}
}
}
private fun updateRearDisplayAvailability(status: @WindowAreaComponent.WindowAreaStatus Int) {
val windowMetrics =
- if (vendorApiLevel >= 3) {
- WindowMetricsCalculator.fromDisplayMetrics(
- displayMetrics = windowAreaComponent.rearDisplayMetrics
- )
- } else {
- val displayMetrics =
- DeviceUtils.getRearDisplayMetrics(Build.MANUFACTURER, Build.MODEL)
- if (displayMetrics != null) {
- WindowMetricsCalculator.fromDisplayMetrics(displayMetrics = displayMetrics)
- } else {
- throw IllegalArgumentException(
- "DeviceUtils rear display metrics entry should not be null"
- )
- }
- }
+ WindowMetricsCalculator.fromDisplayMetrics(
+ displayMetrics = windowAreaComponent.rearDisplayMetrics
+ )
currentRearDisplayModeStatus = WindowAreaAdapter.translate(status, activeWindowAreaSession)
updateRearDisplayWindowArea(
@@ -393,7 +378,8 @@
windowAreaPresentationSessionCallback.onSessionStarted(
RearDisplayPresentationSessionPresenterImpl(
windowAreaComponent,
- windowAreaComponent.rearDisplayPresentation!!
+ windowAreaComponent.rearDisplayPresentation!!,
+ ExtensionsUtil.safeVendorApiLevel
)
)
}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
index af47bd5..14620ae 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
@@ -22,6 +22,7 @@
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.area.WindowAreaComponent
import androidx.window.layout.WindowMetrics
@@ -86,7 +87,8 @@
OPERATION_PRESENT_ON_AREA ->
RearDisplayPresentationSessionPresenterImpl(
windowAreaComponent,
- windowAreaComponent.rearDisplayPresentation!!
+ windowAreaComponent.rearDisplayPresentation!!,
+ ExtensionsUtil.safeVendorApiLevel
)
else -> {
throw IllegalArgumentException("Invalid operation provided")
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
index cd45d92..c9b7a01 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.view.View
+import android.view.Window
import androidx.window.core.ExperimentalWindowApi
/**
@@ -34,6 +35,15 @@
val context: Context
/**
+ * Returns the [Window] associated with the active presentation window area or null if there is
+ * no [Window] currently active. This could occur if the presenter has already been dismissed,
+ * and there is no expectation that the [Window] would become non-null at a later point. This
+ * API can be used to directly access parts of the [Window] API that are not available through
+ * the [Context] provided.
+ */
+ val window: Window?
+
+ /**
* Sets a [View] to show on a window area. After setting the view the system can turn on the
* corresponding display and start showing content.
*/
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
deleted file mode 100644
index 0ab78c0..0000000
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2023 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.reflectionguard;
-
-import android.app.Activity;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.window.extensions.area.WindowAreaComponent;
-import androidx.window.extensions.core.util.function.Consumer;
-
-/**
- * This file defines the Vendor API Level 2 Requirements for WindowAreaComponent. This is used
- * in the client library to perform reflection guard to ensure that the OEM extension implementation
- * is complete.
- *
- * @see WindowAreaComponent
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface WindowAreaComponentApi2Requirements {
-
- /** @see WindowAreaComponent#addRearDisplayStatusListener */
- void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
-
- /** @see WindowAreaComponent#removeRearDisplayStatusListener */
- void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
-
- /** @see WindowAreaComponent#startRearDisplaySession */
- void startRearDisplaySession(@NonNull Activity activity,
- @NonNull Consumer<Integer> consumer);
-
- /** @see WindowAreaComponent#endRearDisplaySession */
- void endRearDisplaySession();
-}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
index 56c54aa..eab11a7 100644
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -36,7 +36,20 @@
* @see WindowAreaComponent
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface WindowAreaComponentApi3Requirements extends WindowAreaComponentApi2Requirements {
+public interface WindowAreaComponentApi3Requirements {
+
+ /** @see WindowAreaComponent#addRearDisplayStatusListener */
+ void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#removeRearDisplayStatusListener */
+ void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#startRearDisplaySession */
+ void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#endRearDisplaySession */
+ void endRearDisplaySession();
/** @see WindowAreaComponent#addRearDisplayPresentationStatusListener */
void addRearDisplayPresentationStatusListener(
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
index 1ab5cdd..fe135be 100644
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
@@ -24,46 +24,47 @@
internal object WindowAreaComponentValidator {
internal fun isWindowAreaComponentValid(windowAreaComponent: Class<*>, apiLevel: Int): Boolean {
- return when {
- apiLevel <= 1 -> false
- apiLevel == 2 ->
- validateImplementation(
- windowAreaComponent,
- WindowAreaComponentApi2Requirements::class.java
- )
- else ->
- validateImplementation(
- windowAreaComponent,
- WindowAreaComponentApi3Requirements::class.java
- )
- }
+ val isWindowAreaComponentValid: Boolean =
+ when {
+ apiLevel <= 2 -> false
+ else ->
+ validateImplementation(
+ windowAreaComponent,
+ WindowAreaComponentApi3Requirements::class.java
+ )
+ }
+ return isWindowAreaComponentValid
}
internal fun isExtensionWindowAreaStatusValid(
extensionWindowAreaStatus: Class<*>,
apiLevel: Int
): Boolean {
- return when {
- apiLevel <= 1 -> false
- else ->
- validateImplementation(
- extensionWindowAreaStatus,
- ExtensionWindowAreaStatusRequirements::class.java
- )
- }
+ val isExtensionWindowAreaStatusValid: Boolean =
+ when {
+ apiLevel <= 2 -> false
+ else ->
+ validateImplementation(
+ extensionWindowAreaStatus,
+ ExtensionWindowAreaStatusRequirements::class.java
+ )
+ }
+ return isExtensionWindowAreaStatusValid
}
internal fun isExtensionWindowAreaPresentationValid(
extensionWindowAreaPresentation: Class<*>,
apiLevel: Int
): Boolean {
- return when {
- apiLevel <= 2 -> false
- else ->
- validateImplementation(
- extensionWindowAreaPresentation,
- ExtensionWindowAreaPresentation::class.java
- )
- }
+ val isExtensionWindowAreaPresentationValid: Boolean =
+ when {
+ apiLevel <= 2 -> false
+ else ->
+ validateImplementation(
+ extensionWindowAreaPresentation,
+ ExtensionWindowAreaPresentation::class.java
+ )
+ }
+ return isExtensionWindowAreaPresentationValid
}
}
diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
deleted file mode 100644
index 359e957..0000000
--- a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2023 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.utils
-
-import android.util.DisplayMetrics
-
-/** Data class holding metrics about a specific device. */
-internal class DeviceMetrics(
- val manufacturer: String,
- val model: String,
- val rearDisplayMetrics: DisplayMetrics
-) {
- override fun equals(other: Any?): Boolean {
- return other is DeviceMetrics &&
- manufacturer == other.manufacturer &&
- model == other.model &&
- rearDisplayMetrics.equals(other.rearDisplayMetrics)
- }
-
- override fun hashCode(): Int {
- var result = manufacturer.hashCode()
- result = 31 * result + model.hashCode()
- result = 31 * result + rearDisplayMetrics.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "DeviceMetrics{ Manufacturer: $manufacturer, model: $model, " +
- "Rear display metrics: $rearDisplayMetrics }"
- }
-}
diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
deleted file mode 100644
index 33f241d..0000000
--- a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2023 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.utils
-
-import android.util.DisplayMetrics
-import java.util.Locale
-
-/**
- * Utility object to provide information about specific devices that may not be available through
- * the extensions API at a certain vendor API level
- */
-internal object DeviceUtils {
-
- private val deviceList =
- listOf(
- DeviceMetrics(
- "google",
- "pixel fold",
- DisplayMetrics().apply {
- widthPixels = 1080
- heightPixels = 2092
- density = 2.625f
- densityDpi = DisplayMetrics.DENSITY_420
- }
- )
- )
-
- internal fun hasDeviceMetrics(manufacturer: String, model: String): Boolean {
- return deviceList.any {
- it.manufacturer == manufacturer.lowercase(Locale.US) &&
- it.model == model.lowercase(Locale.US)
- }
- }
-
- internal fun getRearDisplayMetrics(manufacturer: String, model: String): DisplayMetrics? {
- return deviceList
- .firstOrNull {
- it.manufacturer == manufacturer.lowercase(Locale.US) &&
- it.model == model.lowercase(Locale.US)
- }
- ?.rearDisplayMetrics
- }
-}
diff --git a/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
new file mode 100644
index 0000000..593fb4c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 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.utils
+
+import android.annotation.SuppressLint
+import android.view.Window
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import java.lang.reflect.Method
+
+internal object PresentationWindowCompatUtils {
+
+ // We perform our own extensions vendor API level check at the call-site
+ @SuppressLint("BanUncheckedReflection")
+ fun getWindowBeforeVendorApiLevel4(
+ extensionPresentation: ExtensionWindowAreaPresentation
+ ): Window? {
+ val getWindowMethod = getWindowMethod(extensionPresentation)
+ return if (getWindowMethod == null) null
+ else (getWindowMethod.invoke(extensionPresentation) as Window?)
+ }
+
+ private fun getWindowMethod(extensionPresentation: ExtensionWindowAreaPresentation): Method? {
+ return extensionPresentation.javaClass.methods.firstOrNull { method: Method? ->
+ method?.name == "getWindow" && method.returnType == android.view.Window::class.java
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/core/Bounds.kt b/window/window/src/main/java/androidx/window/core/Bounds.kt
index f821baa..e31ed65 100644
--- a/window/window/src/main/java/androidx/window/core/Bounds.kt
+++ b/window/window/src/main/java/androidx/window/core/Bounds.kt
@@ -28,10 +28,10 @@
* contain any behavior or calculations.
*/
internal class Bounds(
- public val left: Int,
- public val top: Int,
- public val right: Int,
- public val bottom: Int
+ val left: Int,
+ val top: Int,
+ val right: Int,
+ val bottom: Int,
) {
constructor(rect: Rect) : this(rect.left, rect.top, rect.right, rect.bottom)
@@ -88,4 +88,8 @@
result = 31 * result + bottom
return result
}
+
+ companion object {
+ val EMPTY_BOUNDS = Bounds(0, 0, 0, 0)
+ }
}
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
index 32c6e07..3f6655e 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
@@ -17,11 +17,14 @@
package androidx.window.embedding
import android.app.Activity
-import android.app.ActivityOptions
import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
+import androidx.core.util.Consumer
import androidx.window.RequiresWindowSdkExtension
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.WindowSdkExtensions
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
/** The controller that allows checking the current [Activity] embedding status. */
class ActivityEmbeddingController internal constructor(private val backend: EmbeddingBackend) {
@@ -31,7 +34,6 @@
*
* @param activity the [Activity] to check.
*/
- // TODO(b/204399167) Migrate to a Flow
fun isActivityEmbedded(activity: Activity): Boolean = backend.isActivityEmbedded(activity)
/**
@@ -39,27 +41,136 @@
* embedding container and associated with a [SplitInfo]. Returns `null` if there is no such
* [ActivityStack].
*
+ * Started from [WindowSdkExtensions.extensionVersion] 5, this method can also obtain standalone
+ * [ActivityStack], which is not associated with any [SplitInfo]. For example, an
+ * [ActivityStack] launched with [ActivityRule.alwaysExpand], or an overlay [ActivityStack]
+ * launched by [setLaunchingActivityStack] with [OverlayCreateParams].
+ *
* @param activity The [Activity] to check.
* @return the [ActivityStack] that this [activity] is part of, or `null` if there is no such
* [ActivityStack].
*/
- @ExperimentalWindowApi
fun getActivityStack(activity: Activity): ActivityStack? = backend.getActivityStack(activity)
/**
- * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].
+ * Sets the launching [ActivityStack] to the given [Bundle].
*
- * @param options The [android.app.ActivityOptions] to be updated.
- * @param token The token of the [ActivityStack] to be set.
+ * Apps can launch an [Activity] into the [ActivityStack] by [Activity.startActivity].
+ *
+ * @param options the [Bundle] to be updated.
+ * @param activityStack the [ActivityStack] to be set.
+ */
+ @RequiresWindowSdkExtension(5)
+ internal fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle =
+ backend.setLaunchingActivityStack(options, activityStack)
+
+ /**
+ * Finishes a set of [activityStacks][ActivityStack] from the lowest to the highest z-order
+ * regardless of the order of `activityStack` passed in the input parameter.
+ *
+ * If a remaining activityStack from a split participates in other splits with other
+ * activityStacks, the remaining activityStack might split with other activityStacks. For
+ * example, if activityStack A splits with activityStack B and C, and activityStack C covers
+ * activityStack B, finishing activityStack C might make the split of activityStack A and B
+ * show.
+ *
+ * If all split-associated activityStacks are finished, the remaining activityStack will be
+ * expanded to fill the parent task container. This is useful to expand the primary container as
+ * the sample linked below shows.
+ *
+ * **Note** that it's caller's responsibility to check whether this API is supported by checking
+ * [WindowSdkExtensions.extensionVersion] is greater than or equal to 5. If not, an alternative
+ * approach to finishing all containers above a particular activity can be to launch it again
+ * with flag [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].
+ *
+ * @param activityStacks The set of [ActivityStack] to be finished.
+ * @throws UnsupportedOperationException if extension version is less than 5.
+ * @sample androidx.window.samples.embedding.expandPrimaryContainer
+ */
+ @RequiresWindowSdkExtension(5)
+ fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ backend.finishActivityStacks(activityStacks)
+ }
+
+ /**
+ * Sets the [EmbeddingConfiguration] of the Activity Embedding environment that defines how the
+ * embedded Activities behaves.
+ *
+ * The [EmbeddingConfiguration] can be supported only if the vendor API level of the target
+ * device is equals or higher than required API level. Otherwise, it would be no-op when setting
+ * the [EmbeddingConfiguration] on a target device that has lower API level.
+ *
+ * In addition, the existing configuration in the library won't be overwritten if the properties
+ * of the given [embeddingConfiguration] are undefined. Only the configuration properties that
+ * are explicitly set will be updated.
+ *
+ * **Note** that it is recommended to be configured in the [androidx.startup.Initializer] or
+ * [android.app.Application.onCreate], so that the [EmbeddingConfiguration] is applied early in
+ * the application startup, before any activities complete initialization. The
+ * [EmbeddingConfiguration] updates afterward may or may not apply to already running
+ * activities.
+ *
+ * @param embeddingConfiguration The [EmbeddingConfiguration]
+ */
+ @RequiresWindowSdkExtension(5)
+ fun setEmbeddingConfiguration(embeddingConfiguration: EmbeddingConfiguration) {
+ backend.setEmbeddingConfiguration(embeddingConfiguration)
+ }
+
+ /**
+ * Triggers calculator functions set through [SplitController.setSplitAttributesCalculator] and
+ * [OverlayController.setOverlayAttributesCalculator] to update attributes for visible
+ * [activityStacks][ActivityStack].
+ *
+ * This method can be used when the application wants to update the embedding presentation based
+ * on the application state.
+ *
+ * This method is not needed for changes that are driven by window and device state changes or
+ * new activity starts, because those will invoke the calculator functions automatically.
+ *
+ * Visible [activityStacks][ActivityStack] are usually the last element of [SplitInfo] list
+ * which was received from the callback registered in [SplitController.splitInfoList] and an
+ * active overlay [ActivityStack] if exists.
+ *
+ * The call will be no-op if there is no visible [activityStacks][ActivityStack] or there's no
+ * calculator set.
+ *
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 3.
+ * @see androidx.window.embedding.OverlayController.setOverlayAttributesCalculator
+ * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
*/
@RequiresWindowSdkExtension(3)
- internal fun setLaunchingActivityStack(
- options: ActivityOptions,
- token: IBinder
- ): ActivityOptions {
- return backend.setLaunchingActivityStack(options, token)
+ fun invalidateVisibleActivityStacks() {
+ backend.invalidateVisibleActivityStacks()
}
+ /**
+ * A [Flow] of [EmbeddedActivityWindowInfo] that reports the change to the embedded window
+ * related info of the [activity].
+ *
+ * The [Flow] will immediately be invoked with the latest value upon registration if the
+ * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
+ *
+ * When the [activity] is embedded, the [Flow] will be invoked when [EmbeddedActivityWindowInfo]
+ * is changed. When the [activity] is not embedded, the [Flow] will not be triggered unless the
+ * [activity] is becoming non-embedded from embedded.
+ *
+ * Note that this API is only supported on the device with
+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+ * [WindowSdkExtensions.extensionVersion] is less than 6, this [Flow] will not be invoked.
+ *
+ * @param activity the [Activity] that is interested in getting the embedded window info.
+ * @return a [Flow] of [EmbeddedActivityWindowInfo] of the [activity].
+ */
+ @RequiresWindowSdkExtension(6)
+ fun embeddedActivityWindowInfo(activity: Activity): Flow<EmbeddedActivityWindowInfo> =
+ callbackFlow {
+ val callback = Consumer { info: EmbeddedActivityWindowInfo -> trySend(info) }
+ backend.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
+ awaitClose { backend.removeEmbeddedActivityWindowInfoCallbackForActivity(callback) }
+ }
+
companion object {
/**
* Obtains an instance of [ActivityEmbeddingController].
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
new file mode 100644
index 0000000..b3581ef
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 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:JvmName("ActivityEmbeddingOptions")
+
+package androidx.window.embedding
+
+import android.app.Activity
+import android.app.ActivityOptions
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+
+/**
+ * Sets the target launching [ActivityStack] to the given [Bundle].
+ *
+ * The [Bundle] then could be used to launch an [Activity] to the top of the [ActivityStack] through
+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle], it's
+ * suggested to use the bundle to call this method.
+ *
+ * It is suggested to use a visible [ActivityStack] reported by [SplitController.splitInfoList] or
+ * [OverlayController.overlayInfo], or the launching activity will be launched on the default target
+ * if the [activityStack] no longer exists in the host task. The default target could be the top of
+ * the visible split's secondary [ActivityStack], or the top of the host task.
+ *
+ * Below samples are use cases to specify the launching [ActivityStack].
+ *
+ * @sample androidx.window.samples.embedding.launchingOnPrimaryActivityStack
+ * @sample androidx.window.samples.embedding.launchingOnOverlayActivityStack
+ * @param context The [android.content.Context] that is going to be used for launching activity with
+ * this [Bundle], which is usually be the [android.app.Activity] of the app that hosts the task.
+ * @param activityStack The target [ActivityStack] for launching.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 5.
+ */
+@RequiresWindowSdkExtension(5)
+fun Bundle.setLaunchingActivityStack(context: Context, activityStack: ActivityStack): Bundle =
+ ActivityEmbeddingController.getInstance(context).setLaunchingActivityStack(this, activityStack)
+
+/**
+ * Puts [OverlayCreateParams] to [Bundle] to create a singleton-per-task overlay [ActivityStack].
+ *
+ * The [Bundle] then could be used to launch an [Activity] to the [ActivityStack] through
+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle], it's
+ * suggested to use the bundle to call this method.
+ *
+ * Below sample shows how to launch an overlay [ActivityStack].
+ *
+ * If there's an existing overlay [ActivityStack] shown, the existing overlay container may be
+ * dismissed or updated based on [OverlayCreateParams.tag] and [activity] because of following
+ * constraints:
+ * 1. A task can hold only one overlay container at most.
+ * 2. An overlay [ActivityStack] tag is unique per process.
+ *
+ * Belows are possible scenarios:
+ * 1. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in the same
+ * task as [activity], the overlay container's [OverlayAttributes] will be updated to
+ * [OverlayCreateParams.overlayAttributes], and the activity will be launched on the top of the
+ * overlay [ActivityStack].
+ * 2. If there's an overlay container with different `tag` from [OverlayCreateParams.tag] in the
+ * same task as [activity], the existing overlay container will be dismissed, and a new overlay
+ * container will be launched with the new [OverlayCreateParams].
+ * 3. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in a
+ * different task from [activity], the existing overlay container in the other task will be
+ * dismissed, and a new overlay container will be launched in the task of [activity].
+ *
+ * Note that the second and the third scenarios may happen at the same time if [activity]'s task
+ * holds an overlay container and [OverlayCreateParams.tag] matches an overlay container in a
+ * different task.
+ *
+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
+ * @param activity The [Activity] that is going to be used for launching activity with this
+ * [ActivityOptions], which is usually be the [Activity] of the app that hosts the task.
+ * @param overlayCreateParams The parameter container to create an overlay [ActivityStack]
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 6.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+fun Bundle.setOverlayCreateParams(
+ activity: Activity,
+ overlayCreateParams: OverlayCreateParams
+): Bundle =
+ OverlayController.getInstance(activity).setOverlayCreateParams(this, overlayCreateParams)
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
new file mode 100644
index 0000000..34b71fe
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.os.Bundle
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.EmbeddingBounds.Dimension
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG
+import androidx.window.extensions.embedding.ActivityStack.Token
+
+/**
+ * The implementation of ActivityEmbeddingOptions in WM Jetpack which uses constants defined in
+ * [androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties] and this object.
+ */
+internal object ActivityEmbeddingOptionsImpl {
+
+ /**
+ * Key of [EmbeddingBounds].
+ *
+ * Type: [Bundle]
+ *
+ * Properties:
+ *
+ * | Key | Type | Property |
+ * |----------------------------------|----------|-----------------------------|
+ * | [KEY_EMBEDDING_BOUNDS_ALIGNMENT] | [Int] | [EmbeddingBounds.alignment] |
+ * | [KEY_EMBEDDING_BOUNDS_WIDTH] | [Bundle] | [EmbeddingBounds.width] |
+ * | [KEY_EMBEDDING_BOUNDS_HEIGHT] | [Bundle] | [EmbeddingBounds.height] |
+ */
+ private const val KEY_EMBEDDING_BOUNDS = "androidx.window.embedding.EmbeddingBounds"
+
+ /**
+ * Key of [EmbeddingBounds.alignment].
+ *
+ * Type: [Int]
+ *
+ * Valid values are:
+ * - 0: [EmbeddingBounds.Alignment.ALIGN_LEFT]
+ * - 1: [EmbeddingBounds.Alignment.ALIGN_TOP]
+ * - 2: [EmbeddingBounds.Alignment.ALIGN_RIGHT]
+ * - 3: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]
+ */
+ private const val KEY_EMBEDDING_BOUNDS_ALIGNMENT =
+ "androidx.window.embedding.EmbeddingBounds.alignment"
+
+ /**
+ * Key of [EmbeddingBounds.width].
+ *
+ * Type: [Bundle] with [putDimension]
+ *
+ * Properties:
+ *
+ * | Key | Type | Property |
+ * |----------------------------------------|----------------|---------------------|
+ * | [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] | [String] | The dimension type |
+ * | [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] | [Int], [Float] | The dimension value |
+ */
+ private const val KEY_EMBEDDING_BOUNDS_WIDTH = "androidx.window.embedding.EmbeddingBounds.width"
+
+ /**
+ * Key of [EmbeddingBounds.width].
+ *
+ * Type: [Bundle] with [putDimension]
+ *
+ * Properties:
+ *
+ * | Key | Type | Property |
+ * |----------------------------------------|----------------|---------------------|
+ * | [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] | [String] | The dimension type |
+ * | [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] | [Int], [Float] | The dimension value |
+ */
+ private const val KEY_EMBEDDING_BOUNDS_HEIGHT =
+ "androidx.window.embedding.EmbeddingBounds.height"
+
+ /**
+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+ * [Dimension] is [DIMENSION_EXPANDED].
+ */
+ private const val DIMENSION_TYPE_EXPANDED = "expanded"
+
+ /**
+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+ * [Dimension] is [DIMENSION_HINGE].
+ */
+ private const val DIMENSION_TYPE_HINGE = "hinge"
+
+ /**
+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+ * [Dimension] is from [Dimension.ratio]. If this type is used,
+ * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Float] value.
+ */
+ private const val DIMENSION_TYPE_RATIO = "ratio"
+
+ /**
+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+ * [Dimension] is from [Dimension.pixel]. If this type is used,
+ * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Int] value.
+ */
+ private const val DIMENSION_TYPE_PIXEL = "pixel"
+
+ /**
+ * Key of [EmbeddingBounds.Dimension] type.
+ *
+ * Type: [String]
+ *
+ * Valid values are:
+ * - [DIMENSION_TYPE_EXPANDED]: [DIMENSION_EXPANDED]
+ * - [DIMENSION_TYPE_HINGE]: [DIMENSION_HINGE]
+ * - [DIMENSION_TYPE_RATIO]: [Dimension.ratio]
+ * - [DIMENSION_TYPE_PIXEL]: [Dimension.pixel]
+ */
+ private const val KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE =
+ "androidx.window.embedding.EmbeddingBounds.dimension_type"
+
+ /**
+ * Key of [EmbeddingBounds.Dimension] value.
+ *
+ * Type: [Float] or [Int]
+ *
+ * The value passed in [Dimension.pixel] or [Dimension.ratio]:
+ * - Accept [Float] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_RATIO]
+ * - Accept [Int] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_PIXEL]
+ */
+ private const val KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE =
+ "androidx.window.embedding.EmbeddingBounds.dimension_value"
+
+ /**
+ * Key of [ActivityStack] alignment relative to the parent container.
+ *
+ * Type: [Int]
+ *
+ * Valid values are:
+ * - 0: [EmbeddingBounds.Alignment.ALIGN_LEFT]
+ * - 1: [EmbeddingBounds.Alignment.ALIGN_TOP]
+ * - 2: [EmbeddingBounds.Alignment.ALIGN_RIGHT]
+ * - 3: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]
+ */
+ private const val KEY_ACTIVITY_STACK_ALIGNMENT =
+ "androidx.window.embedding.ActivityStackAlignment"
+
+ /**
+ * Puts [OverlayCreateParams] information to [android.app.ActivityOptions] bundle to launch the
+ * overlay container
+ *
+ * @param overlayCreateParams The [OverlayCreateParams] to launch the overlay container
+ */
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ internal fun setOverlayCreateParams(
+ options: Bundle,
+ overlayCreateParams: OverlayCreateParams,
+ ) {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ options.putString(KEY_OVERLAY_TAG, overlayCreateParams.tag)
+ options.putEmbeddingBounds(overlayCreateParams.overlayAttributes.bounds)
+ }
+
+ /** Puts [EmbeddingBounds] information into a bundle for tracking. */
+ private fun Bundle.putEmbeddingBounds(embeddingBounds: EmbeddingBounds) {
+ putBundle(
+ KEY_EMBEDDING_BOUNDS,
+ Bundle().apply {
+ putInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT, embeddingBounds.alignment.value)
+ putDimension(KEY_EMBEDDING_BOUNDS_WIDTH, embeddingBounds.width)
+ putDimension(KEY_EMBEDDING_BOUNDS_HEIGHT, embeddingBounds.height)
+ }
+ )
+ }
+
+ /**
+ * Puts the alignment of the overlay [ActivityStack] relative to its parent container.
+ *
+ * It could be used as a hint of the open/close animation direction.
+ */
+ internal fun Bundle.putActivityStackAlignment(embeddingBounds: EmbeddingBounds) {
+ putInt(KEY_ACTIVITY_STACK_ALIGNMENT, embeddingBounds.alignment.value)
+ }
+
+ internal fun Bundle.getOverlayAttributes(): OverlayAttributes? {
+ val embeddingBounds = getEmbeddingBounds() ?: return null
+ return OverlayAttributes(embeddingBounds)
+ }
+
+ private fun Bundle.getEmbeddingBounds(): EmbeddingBounds? {
+ val embeddingBoundsBundle = getBundle(KEY_EMBEDDING_BOUNDS) ?: return null
+ return EmbeddingBounds(
+ EmbeddingBounds.Alignment(embeddingBoundsBundle.getInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT)),
+ embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_WIDTH),
+ embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_HEIGHT)
+ )
+ }
+
+ /**
+ * Retrieves [EmbeddingBounds.Dimension] value from bundle with given [key].
+ *
+ * See [putDimension] for the data structure of [EmbeddingBounds.Dimension] as bundle
+ */
+ private fun Bundle.getDimension(key: String): Dimension {
+ val dimensionBundle = getBundle(key)!!
+ return when (val type = dimensionBundle.getString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE)) {
+ DIMENSION_TYPE_EXPANDED -> DIMENSION_EXPANDED
+ DIMENSION_TYPE_HINGE -> DIMENSION_HINGE
+ DIMENSION_TYPE_RATIO ->
+ Dimension.ratio(dimensionBundle.getFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
+ DIMENSION_TYPE_PIXEL ->
+ Dimension.pixel(dimensionBundle.getInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
+ else -> throw IllegalArgumentException("Illegal type $type")
+ }
+ }
+
+ /**
+ * Puts [EmbeddingBounds.Dimension] information into bundle with a given [key].
+ *
+ * [EmbeddingBounds.Dimension] is encoded as a [Bundle] with following data structure:
+ * - [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]: A `string` type. Must be one of:
+ * - [DIMENSION_TYPE_EXPANDED]
+ * - [DIMENSION_TYPE_HINGE]
+ * - [DIMENSION_TYPE_RATIO]
+ * - [DIMENSION_TYPE_PIXEL]
+ * - [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]: Only specified for [DIMENSION_TYPE_RATIO] and
+ * [DIMENSION_TYPE_PIXEL]. [DIMENSION_TYPE_RATIO] requires a [Float], while
+ * [DIMENSION_TYPE_PIXEL] requires a [Int].
+ */
+ private fun Bundle.putDimension(key: String, dimension: Dimension) {
+ putBundle(
+ key,
+ Bundle().apply {
+ when (dimension) {
+ DIMENSION_EXPANDED -> {
+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_EXPANDED)
+ }
+ DIMENSION_HINGE -> {
+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_HINGE)
+ }
+ is Dimension.Ratio -> {
+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_RATIO)
+ putFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
+ }
+ is Dimension.Pixel -> {
+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_PIXEL)
+ putInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
+ }
+ }
+ }
+ )
+ }
+
+ @RequiresWindowSdkExtension(5)
+ internal fun setActivityStackToken(options: Bundle, activityStackToken: Token) {
+ options.putBundle(KEY_ACTIVITY_STACK_TOKEN, activityStackToken.toBundle())
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
index 24b369a..392dd6c 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -17,15 +17,16 @@
import android.app.Activity
import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.embedding.ActivityStack.Token
/**
* A container that holds a stack of activities, overlapping and bound to the same rectangle on the
* screen.
*/
class ActivityStack
-@RestrictTo(LIBRARY_GROUP)
-constructor(
+internal constructor(
/**
* The [Activity] list in this application's process that belongs to this [ActivityStack].
*
@@ -42,8 +43,29 @@
* `false`.
*/
val isEmpty: Boolean,
+ /** A token uniquely identifying this `ActivityStack`. */
+ private val token: Token?,
) {
+ /**
+ * Creates ActivityStack ONLY for testing.
+ *
+ * @param activitiesInProcess the [Activity] list in this application's process that belongs to
+ * this [ActivityStack].
+ * @param isEmpty whether there is no [Activity] running in this [ActivityStack].
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ constructor(
+ activitiesInProcess: List<Activity>,
+ isEmpty: Boolean
+ ) : this(activitiesInProcess, isEmpty, token = null)
+
+ @RequiresWindowSdkExtension(5)
+ internal fun getToken(): Token = let {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+ token!!
+ }
+
/** Whether this [ActivityStack] contains the [activity]. */
operator fun contains(activity: Activity): Boolean {
return activitiesInProcess.contains(activity)
@@ -55,6 +77,7 @@
if (activitiesInProcess != other.activitiesInProcess) return false
if (isEmpty != other.isEmpty) return false
+ if (token != other.token) return false
return true
}
@@ -62,9 +85,14 @@
override fun hashCode(): Int {
var result = activitiesInProcess.hashCode()
result = 31 * result + isEmpty.hashCode()
+ result = 31 * result + token.hashCode()
return result
}
override fun toString(): String =
- "ActivityStack{" + "activitiesInProcess=$activitiesInProcess" + ", isEmpty=$isEmpty" + "}"
+ "ActivityStack{" +
+ "activitiesInProcess=$activitiesInProcess" +
+ ", isEmpty=$isEmpty" +
+ ", token=$token" +
+ "}"
}
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
new file mode 100644
index 0000000..c63928f
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.app.Activity
+import android.graphics.Rect
+import android.util.ArrayMap
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo
+import androidx.window.reflection.Consumer2
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/** Manages and dispatches update of [EmbeddedActivityWindowInfo]. */
+@RequiresWindowSdkExtension(6)
+internal open class ActivityWindowInfoCallbackController(
+ private val embeddingExtension: ActivityEmbeddingComponent,
+) {
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock") private val extensionsCallback: Consumer<ExtensionsActivityWindowInfo>
+
+ @VisibleForTesting
+ @GuardedBy("globalLock")
+ internal var callbacks:
+ MutableMap<JetpackConsumer<EmbeddedActivityWindowInfo>, CallbackWrapper> =
+ ArrayMap()
+
+ init {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+ extensionsCallback =
+ Consumer2<ExtensionsActivityWindowInfo> { info ->
+ globalLock.withLock {
+ for (callbackWrapper in callbacks.values) {
+ callbackWrapper.accept(info)
+ }
+ }
+ }
+ }
+
+ fun addCallback(activity: Activity, callback: JetpackConsumer<EmbeddedActivityWindowInfo>) {
+ globalLock.withLock {
+ if (callbacks.isEmpty()) {
+ // Register when the first callback is added.
+ embeddingExtension.setEmbeddedActivityWindowInfoCallback(
+ Runnable::run,
+ extensionsCallback
+ )
+ }
+
+ val callbackWrapper = CallbackWrapper(activity, callback)
+ callbacks[callback] = callbackWrapper
+ embeddingExtension.getEmbeddedActivityWindowInfo(activity)?.apply {
+ // Trigger with the latest info if the window exists.
+ callbackWrapper.accept(this)
+ }
+ }
+ }
+
+ fun removeCallback(callback: JetpackConsumer<EmbeddedActivityWindowInfo>) {
+ globalLock.withLock {
+ if (callbacks.remove(callback) == null) {
+ // Early return if the callback is not registered.
+ return
+ }
+ if (callbacks.isEmpty()) {
+ // Unregister when the last callback is removed.
+ embeddingExtension.clearEmbeddedActivityWindowInfoCallback()
+ }
+ }
+ }
+
+ /** Translates from Extensions info to Jetpack info. */
+ @VisibleForTesting
+ internal open fun translate(info: ExtensionsActivityWindowInfo): EmbeddedActivityWindowInfo {
+ val parentHostBounds = Rect(info.taskBounds)
+ val boundsInParentHost = Rect(info.activityStackBounds)
+ // Converting to host container coordinate.
+ boundsInParentHost.offset(-parentHostBounds.left, -parentHostBounds.top)
+ return EmbeddedActivityWindowInfo(
+ isEmbedded = info.isEmbedded,
+ parentHostBounds = parentHostBounds,
+ boundsInParentHost = boundsInParentHost
+ )
+ }
+
+ @VisibleForTesting
+ internal inner class CallbackWrapper(
+ private val activity: Activity,
+ val callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+ ) {
+ var lastReportedInfo: EmbeddedActivityWindowInfo? = null
+
+ fun accept(extensionsActivityWindowInfo: ExtensionsActivityWindowInfo) {
+ val updatedActivity = extensionsActivityWindowInfo.activity
+ if (activity != updatedActivity) {
+ return
+ }
+
+ val newInfo = translate(extensionsActivityWindowInfo)
+ if (shouldReportInfo(newInfo)) {
+ lastReportedInfo = newInfo
+ callback.accept(newInfo)
+ }
+ }
+
+ private fun shouldReportInfo(newInfo: EmbeddedActivityWindowInfo): Boolean =
+ lastReportedInfo?.let {
+ if (it.isEmbedded != newInfo.isEmbedded) {
+ // Always report if the embedded status changes
+ return true
+ }
+ if (!newInfo.isEmbedded) {
+ // Do not report if the activity is not embedded
+ return false
+ }
+ return it != newInfo
+ } ?: newInfo.isEmbedded // Always report the first available info if it is embedded
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
new file mode 100644
index 0000000..b319fdf
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.window.RequiresWindowSdkExtension
+
+/**
+ * The attributes of the divider layout and behavior.
+ *
+ * @property widthDp the width of the divider.
+ * @property color the color of the divider.
+ * @see SplitAttributes.Builder.setDividerAttributes
+ * @see FixedDividerAttributes
+ * @see DraggableDividerAttributes
+ * @see NO_DIVIDER
+ */
+abstract class DividerAttributes
+private constructor(
+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) val widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+ @ColorInt val color: Int = Color.BLACK,
+) {
+ override fun toString(): String =
+ DividerAttributes::class.java.simpleName + "{" + "width=$widthDp, " + "color=$color" + "}"
+
+ /**
+ * The attributes of a fixed divider. A fixed divider is a divider type that draws a static line
+ * between the primary and secondary containers.
+ *
+ * @property widthDp the width of the divider.
+ * @property color the color of the divider.
+ * @see SplitAttributes.Builder.setDividerAttributes
+ */
+ class FixedDividerAttributes
+ @RequiresWindowSdkExtension(6)
+ private constructor(
+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+ @ColorInt color: Int = Color.BLACK
+ ) : DividerAttributes(widthDp, color) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is FixedDividerAttributes) return false
+ return widthDp == other.widthDp && color == other.color
+ }
+
+ override fun hashCode(): Int = widthDp * 31 + color
+
+ /**
+ * The [FixedDividerAttributes] builder.
+ *
+ * @constructor creates a new [FixedDividerAttributes.Builder]
+ */
+ @RequiresWindowSdkExtension(6)
+ class Builder() {
+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
+ private var widthDp = WIDTH_SYSTEM_DEFAULT
+
+ @ColorInt private var color = Color.BLACK
+
+ /**
+ * The [FixedDividerAttributes] builder constructor initialized by an existing
+ * [FixedDividerAttributes].
+ *
+ * @param original the original [FixedDividerAttributes] to initialize the [Builder].
+ */
+ @RequiresWindowSdkExtension(6)
+ constructor(original: FixedDividerAttributes) : this() {
+ widthDp = original.widthDp
+ color = original.color
+ }
+
+ /**
+ * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the system
+ * will choose a default value based on the display size and form factor.
+ *
+ * @throws IllegalArgumentException if the provided value is invalid.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
+ apply {
+ validateWidth(widthDp)
+ this.widthDp = widthDp
+ }
+
+ /**
+ * Sets the color of the divider. If not set, the default color [Color.BLACK] is used.
+ *
+ * @throws IllegalArgumentException if the provided value is invalid.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun setColor(@ColorInt color: Int): Builder = apply {
+ validateColor(color)
+ this.color = color
+ }
+
+ /** Builds a [FixedDividerAttributes] instance. */
+ @RequiresWindowSdkExtension(6)
+ fun build(): FixedDividerAttributes {
+ return FixedDividerAttributes(widthDp = widthDp, color = color)
+ }
+ }
+ }
+
+ /**
+ * The attributes of a draggable divider. A draggable divider draws a line between the primary
+ * and secondary containers with a drag handle that the user can drag and resize the containers.
+ *
+ * While dragging, the content of the activity is temporarily covered by a solid color veil,
+ * where the color is determined by the window background color of the activity. Apps may use
+ * [android.app.Activity.getWindow] and [android.view.Window.setBackgroundDrawable] to configure
+ * the veil colors.
+ *
+ * @property widthDp the width of the divider.
+ * @property color the color of the divider.
+ * @property dragRange the range that a divider is allowed to be dragged. When the user drags
+ * the divider beyond this range, the system will choose to either fully expand the container
+ * or move the divider back into the range.
+ * @property isDraggingToFullscreenAllowed if {@code true}, the user is allowed to drag beyond
+ * the specified range temporarily, and when dragging is finished, the system will choose to
+ * either fully expand the larger container or move the divider back to the range limit.
+ * Default to {@code false}.
+ * @see SplitAttributes.Builder.setDividerAttributes
+ */
+ class DraggableDividerAttributes
+ @RequiresWindowSdkExtension(6)
+ private constructor(
+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+ @ColorInt color: Int = Color.BLACK,
+ val dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT,
+ val isDraggingToFullscreenAllowed: Boolean = false,
+ ) : DividerAttributes(widthDp, color) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DraggableDividerAttributes) return false
+ return widthDp == other.widthDp &&
+ color == other.color &&
+ dragRange == other.dragRange &&
+ isDraggingToFullscreenAllowed == other.isDraggingToFullscreenAllowed
+ }
+
+ override fun hashCode(): Int =
+ (((widthDp * 31 + color) * 31 + dragRange.hashCode()) * 31 +
+ isDraggingToFullscreenAllowed.hashCode())
+
+ override fun toString(): String =
+ DraggableDividerAttributes::class.java.simpleName +
+ "{" +
+ "width=$widthDp, " +
+ "color=$color, " +
+ "primaryContainerDragRange=$dragRange, " +
+ "isDraggingToFullscreenAllowed=$isDraggingToFullscreenAllowed" +
+ "}"
+
+ /**
+ * The [DraggableDividerAttributes] builder.
+ *
+ * @constructor creates a new [DraggableDividerAttributes.Builder]
+ */
+ @RequiresWindowSdkExtension(6)
+ class Builder() {
+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
+ private var widthDp = WIDTH_SYSTEM_DEFAULT
+
+ @ColorInt private var color = Color.BLACK
+
+ private var dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT
+
+ private var isDraggingToFullscreenAllowed: Boolean = false
+
+ /**
+ * The [DraggableDividerAttributes] builder constructor initialized by an existing
+ * [DraggableDividerAttributes].
+ *
+ * @param original the original [DraggableDividerAttributes] to initialize the [Builder]
+ */
+ @RequiresWindowSdkExtension(6)
+ constructor(original: DraggableDividerAttributes) : this() {
+ widthDp = original.widthDp
+ dragRange = original.dragRange
+ color = original.color
+ isDraggingToFullscreenAllowed = original.isDraggingToFullscreenAllowed
+ }
+
+ /**
+ * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the system
+ * will choose a default value based on the display size and form factor.
+ *
+ * @throws IllegalArgumentException if the provided value is invalid.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
+ apply {
+ validateWidth(widthDp)
+ this.widthDp = widthDp
+ }
+
+ /**
+ * Sets the color of the divider. If not set, the default color [Color.BLACK] is used.
+ *
+ * @throws IllegalArgumentException if the provided value is invalid.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun setColor(@ColorInt color: Int): Builder = apply {
+ validateColor(color)
+ this.color = color
+ }
+
+ /**
+ * Sets the drag range of the divider in terms of the split ratio of the primary
+ * container. It defaults to [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], which means the
+ * system will choose a default value based on the display size and form factor.
+ *
+ * When the user drags the divider beyond this range, the system will choose to either
+ * fully expand the container or move the divider back into the range.
+ *
+ * @param dragRange the [DragRange] for the draggable divider.
+ */
+ @RequiresWindowSdkExtension(6)
+ fun setDragRange(dragRange: DragRange): Builder = apply { this.dragRange = dragRange }
+
+ /**
+ * Sets whether dragging to full screen is allowed.
+ *
+ * If `true`, the user is allowed to drag beyond the specified range temporarily. When
+ * dragging is finished, if the dragging position is below the
+ * [DragRange.SplitRatioDragRange.minRatio] or the default min ratio in
+ * [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], the system will choose to either fully expand
+ * the secondary container or move the divider back to the range limit; if the dragging
+ * position is above the [DragRange.SplitRatioDragRange.maxRatio] or the default max
+ * ratio in [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], the system will choose to either
+ * fully expand the primary container or move the divider back to the range limit.
+ *
+ * When the primary container is fully expanded, the secondary container is dismissed.
+ * When the secondary container is fully expanded, the primary container is hidden
+ * behind the secondary container, and the drag handle is displayed on the edge to allow
+ * the user to drag and bring back the primary container.
+ *
+ * Default to `false`.
+ *
+ * This is only supported on devices with Window SDK extensions version 7 and above. For
+ * devices with Window SDK extensions below version 7, dragging to fullscreen is always
+ * disabled.
+ */
+ @RequiresWindowSdkExtension(7)
+ fun setDraggingToFullscreenAllowed(allowed: Boolean): Builder = apply {
+ this.isDraggingToFullscreenAllowed = allowed
+ }
+
+ /** Builds a [DividerAttributes] instance. */
+ @RequiresWindowSdkExtension(6)
+ fun build(): DraggableDividerAttributes =
+ DraggableDividerAttributes(
+ widthDp = widthDp,
+ color = color,
+ dragRange = dragRange,
+ isDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed,
+ )
+ }
+ }
+
+ /**
+ * Describes the range that the user is allowed to drag the draggable divider.
+ *
+ * @see SplitRatioDragRange
+ * @see DRAG_RANGE_SYSTEM_DEFAULT
+ */
+ abstract class DragRange private constructor() {
+ /**
+ * A drag range represented as an interval of the primary container's split ratios.
+ *
+ * @constructor constructs a new [SplitRatioDragRange]
+ * @property minRatio the minimum split ratio of the primary container that the user is
+ * allowed to drag to. When the divider is dragged beyond this ratio, the system will
+ * choose to either fully expand the secondary container, or move the divider back to this
+ * ratio.
+ * @property maxRatio the maximum split ratio of the primary container that the user is
+ * allowed to drag to. When the divider is dragged beyond this ratio, the system will
+ * choose to either fully expand the primary container, or move the divider back to this
+ * ratio.
+ * @throws IllegalArgumentException if the provided values are invalid.
+ */
+ class SplitRatioDragRange(
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+ val minRatio: Float,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+ val maxRatio: Float,
+ ) : DragRange() {
+ init {
+ if (minRatio <= 0.0 || minRatio >= 1.0) {
+ throw IllegalArgumentException("minRatio must be in the interval (0.0, 1.0)")
+ }
+ if (maxRatio <= 0.0 || maxRatio >= 1.0) {
+ throw IllegalArgumentException("maxRatio must be in the interval (0.0, 1.0)")
+ }
+ if (minRatio > maxRatio) {
+ throw IllegalArgumentException(
+ "minRatio must be less than or equal to maxRatio"
+ )
+ }
+ }
+
+ override fun toString(): String = "SplitRatioDragRange[$minRatio, $maxRatio]"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SplitRatioDragRange) return false
+ return minRatio == other.minRatio && maxRatio == other.maxRatio
+ }
+
+ override fun hashCode(): Int = minRatio.hashCode() * 31 + maxRatio.hashCode()
+ }
+
+ companion object {
+ /**
+ * A special value to indicate that the system will choose default values based on the
+ * display size and form factor.
+ *
+ * @see DraggableDividerAttributes.dragRange
+ */
+ @JvmField
+ val DRAG_RANGE_SYSTEM_DEFAULT =
+ object : DragRange() {
+ override fun toString(): String = "DRAG_RANGE_SYSTEM_DEFAULT"
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * A special value to indicate that the system will choose a default value based on the
+ * display size and form factor.
+ *
+ * @see DividerAttributes.widthDp
+ */
+ const val WIDTH_SYSTEM_DEFAULT: Int = -1
+
+ /** Indicates that no divider is requested. */
+ @JvmField
+ val NO_DIVIDER =
+ object : DividerAttributes() {
+ override fun toString(): String = "NO_DIVIDER"
+ }
+
+ private fun validateWidth(widthDp: Int) = run {
+ require(widthDp == WIDTH_SYSTEM_DEFAULT || widthDp >= 0) {
+ "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT. Got: $widthDp"
+ }
+ }
+
+ private fun validateColor(@ColorInt color: Int) = run {
+ require(color.alpha() == 255) {
+ "Divider color must be opaque. Got: ${Integer.toHexString(color)}"
+ }
+ }
+
+ /**
+ * Returns the alpha value of the color. This is the same as [Color.alpha] and is used to
+ * avoid test-time dependency.
+ */
+ private fun Int.alpha() = this ushr 24
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
new file mode 100644
index 0000000..f2a3fe6
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.graphics.Rect
+
+/**
+ * Describes the embedded window related info of an activity.
+ *
+ * When the activity is embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will
+ * be invoked when any fields of [EmbeddedActivityWindowInfo] is changed. When the activity is not
+ * embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will not be triggered
+ * unless the activity is becoming non-embedded from embedded, in which case [isEmbedded] will be
+ * `false`.
+ *
+ * @see ActivityEmbeddingController.embeddedActivityWindowInfo
+ */
+class EmbeddedActivityWindowInfo
+internal constructor(
+ /**
+ * Whether this activity is embedded and its presentation may be customized by the host process
+ * of the task it is associated with.
+ */
+ val isEmbedded: Boolean,
+ /**
+ * The bounds of the host container in display coordinate space, which should be the Task bounds
+ * for regular embedding use case, or if the activity is not embedded.
+ */
+ val parentHostBounds: Rect,
+ /**
+ * The relative bounds of the embedded [ActivityStack] in the host container coordinate space.
+ * It has the same size as [parentHostBounds] if the activity is not embedded.
+ */
+ val boundsInParentHost: Rect,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EmbeddedActivityWindowInfo) return false
+
+ if (isEmbedded != other.isEmbedded) return false
+ if (parentHostBounds != other.parentHostBounds) return false
+ if (boundsInParentHost != other.boundsInParentHost) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = isEmbedded.hashCode()
+ result = 31 * result + parentHostBounds.hashCode()
+ result = 31 * result + boundsInParentHost.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "EmbeddedActivityWindowInfo{" +
+ "isEmbedded=$isEmbedded" +
+ ", parentHostBounds=$parentHostBounds" +
+ ", boundsInParentHost=$boundsInParentHost" +
+ "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index 0191f1f..f9c9439 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -20,12 +20,23 @@
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.content.res.Resources
import android.os.Binder
import android.util.LayoutDirection
+import android.util.Log
import android.util.Pair as AndroidPair
-import android.view.WindowMetrics
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
+import androidx.window.core.Bounds
+import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.DividerAttributes.DragRange.Companion.DRAG_RANGE_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
+import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_ACTIVITY_STACK
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
@@ -38,7 +49,13 @@
import androidx.window.embedding.SplitAttributes.SplitType.Companion.ratio
import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
+import androidx.window.extensions.embedding.AnimationParams as OEMEmbeddingAnimationParams
+import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
+import androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT
import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
+import androidx.window.extensions.embedding.ParentContainerInfo as OEMParentContainerInfo
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
@@ -49,71 +66,156 @@
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
import androidx.window.extensions.embedding.SplitPairRule.FINISH_NEVER
+import androidx.window.extensions.embedding.SplitPinRule as OEMSplitPinRule
+import androidx.window.extensions.embedding.SplitPinRule.Builder as SplitPinRuleBuilder
import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
+import androidx.window.extensions.embedding.WindowAttributes as OEMWindowAttributes
+import androidx.window.extensions.embedding.WindowAttributes
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
+import androidx.window.layout.util.DensityCompatHelper
import androidx.window.reflection.JFunction2
import androidx.window.reflection.Predicate2
/** Adapter class that translates data classes between Extension and Jetpack interfaces. */
internal class EmbeddingAdapter(private val predicateAdapter: PredicateAdapter) {
- private val vendorApiLevel
+ private val extensionVersion
get() = WindowSdkExtensions.getInstance().extensionVersion
private val api1Impl = VendorApiLevel1Impl(predicateAdapter)
private val api2Impl = VendorApiLevel2Impl()
+ private val api3Impl = VendorApiLevel3Impl()
+ @OptIn(ExperimentalWindowApi::class) var embeddingConfiguration: EmbeddingConfiguration? = null
fun translate(splitInfoList: List<OEMSplitInfo>): List<SplitInfo> {
return splitInfoList.map(this::translate)
}
- @Suppress("DEPRECATION")
private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
- return when (vendorApiLevel) {
+ return when (extensionVersion) {
1 -> api1Impl.translateCompat(splitInfo)
2 -> api2Impl.translateCompat(splitInfo)
- else -> {
- val primaryActivityStack = splitInfo.primaryActivityStack
- val secondaryActivityStack = splitInfo.secondaryActivityStack
+ in 3..4 -> api3Impl.translateCompat(splitInfo)
+ else ->
SplitInfo(
- ActivityStack(
- primaryActivityStack.activities,
- primaryActivityStack.isEmpty,
- ),
- ActivityStack(
- secondaryActivityStack.activities,
- secondaryActivityStack.isEmpty,
- ),
+ translate(splitInfo.primaryActivityStack),
+ translate(splitInfo.secondaryActivityStack),
translate(splitInfo.splitAttributes),
- splitInfo.token,
+ splitInfo.splitInfoToken,
)
- }
}
}
- internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes =
- SplitAttributes.Builder()
- .setSplitType(
- when (val splitType = splitAttributes.splitType) {
- is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
- is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
- is RatioSplitType -> ratio(splitType.ratio)
- else -> throw IllegalArgumentException("Unknown split type: $splitType")
- }
+ internal fun translate(activityStack: OEMActivityStack): ActivityStack =
+ when (extensionVersion) {
+ in 1..4 -> api1Impl.translateCompat(activityStack)
+ else ->
+ ActivityStack(
+ activityStack.activities,
+ activityStack.isEmpty,
+ activityStack.activityStackToken,
+ )
+ }
+
+ internal fun translate(activityStacks: List<OEMActivityStack>): List<ActivityStack> =
+ activityStacks.map(this::translate)
+
+ @Suppress("DEPRECATION") // To compat with device with extension versions 5 and 6.
+ internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes {
+ val builder =
+ SplitAttributes.Builder()
+ .setSplitType(
+ when (val splitType = splitAttributes.splitType) {
+ is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
+ is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
+ is RatioSplitType -> ratio(splitType.ratio)
+ else -> throw IllegalArgumentException("Unknown split type: $splitType")
+ }
+ )
+ .setLayoutDirection(
+ when (val layoutDirection = splitAttributes.layoutDirection) {
+ OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
+ OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
+ OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
+ OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
+ OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
+ else ->
+ throw IllegalArgumentException(
+ "Unknown layout direction: $layoutDirection"
+ )
+ }
+ )
+ if (extensionVersion in 5..6) {
+ val animationParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ translateToJetpackAnimationBackground(splitAttributes.animationBackground)
+ )
+ .build()
+ builder.setAnimationParams(animationParams)
+ }
+ if (extensionVersion >= 7) {
+ val animationParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ translateToJetpackAnimationBackground(
+ splitAttributes.animationParams.animationBackground
+ )
+ )
+ .setOpenAnimation(
+ translateToJetpackAnimationSpec(
+ splitAttributes.animationParams.openAnimationResId
+ )
+ )
+ .setCloseAnimation(
+ translateToJetpackAnimationSpec(
+ splitAttributes.animationParams.closeAnimationResId
+ )
+ )
+ .setChangeAnimation(
+ translateToJetpackAnimationSpec(
+ splitAttributes.animationParams.changeAnimationResId
+ )
+ )
+ .build()
+ builder.setAnimationParams(animationParams)
+ }
+ if (extensionVersion >= 6) {
+ builder.setDividerAttributes(
+ translateToJetpackDividerAttributes(splitAttributes.dividerAttributes)
)
- .setLayoutDirection(
- when (val layoutDirection = splitAttributes.layoutDirection) {
- OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
- OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
- OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
- OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
- OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
- else ->
- throw IllegalArgumentException("Unknown layout direction: $layoutDirection")
- }
+ }
+ return builder.build()
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ @OptIn(ExperimentalWindowApi::class)
+ @SuppressLint("NewApi", "ClassVerificationFailure")
+ internal fun translate(
+ parentContainerInfo: OEMParentContainerInfo,
+ ): ParentContainerInfo {
+ val configuration = parentContainerInfo.configuration
+ val density =
+ DensityCompatHelper.getInstance()
+ .density(parentContainerInfo.configuration, parentContainerInfo.windowMetrics)
+ val windowMetrics =
+ WindowMetricsCalculator.translateWindowMetrics(
+ parentContainerInfo.windowMetrics,
+ density
)
- .build()
+
+ return ParentContainerInfo(
+ Bounds(windowMetrics.bounds),
+ ExtensionsWindowLayoutInfoAdapter.translate(
+ windowMetrics,
+ parentContainerInfo.windowLayoutInfo
+ ),
+ windowMetrics.getWindowInsets(),
+ configuration,
+ density
+ )
+ }
fun translateSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -123,32 +225,35 @@
}
@SuppressLint("NewApi")
- fun translate(params: OEMSplitAttributesCalculatorParams): SplitAttributesCalculatorParams =
- let {
- val taskWindowMetrics = params.parentWindowMetrics
- val taskConfiguration = params.parentConfiguration
- val windowLayoutInfo = params.parentWindowLayoutInfo
- val defaultSplitAttributes = params.defaultSplitAttributes
- val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
- val splitRuleTag = params.splitRuleTag
- val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
-
- SplitAttributesCalculatorParams(
- windowMetrics,
- taskConfiguration,
- ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
- translate(defaultSplitAttributes),
- areDefaultConstraintsSatisfied,
- splitRuleTag,
- )
- }
+ fun translate(
+ params: OEMSplitAttributesCalculatorParams,
+ ): SplitAttributesCalculatorParams = let {
+ val taskWindowMetrics = params.parentWindowMetrics
+ val taskConfiguration = params.parentConfiguration
+ val windowLayoutInfo = params.parentWindowLayoutInfo
+ val defaultSplitAttributes = params.defaultSplitAttributes
+ val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
+ val splitRuleTag = params.splitRuleTag
+ val density =
+ DensityCompatHelper.getInstance().density(taskConfiguration, taskWindowMetrics)
+ val windowMetrics =
+ WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics, density)
+ SplitAttributesCalculatorParams(
+ windowMetrics,
+ taskConfiguration,
+ ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
+ translate(defaultSplitAttributes),
+ areDefaultConstraintsSatisfied,
+ splitRuleTag,
+ )
+ }
private fun translateSplitPairRule(
context: Context,
rule: SplitPairRule,
predicateClass: Class<*>
): OEMSplitPairRule {
- if (vendorApiLevel < 2) {
+ if (extensionVersion < 2) {
return api1Impl.translateSplitPairRuleCompat(context, rule, predicateClass)
} else {
val activitiesPairPredicate =
@@ -167,7 +272,7 @@
}
}
val windowMetricsPredicate =
- Predicate2<WindowMetrics> { windowMetrics ->
+ Predicate2<AndroidWindowMetrics> { windowMetrics ->
rule.checkParentMetrics(context, windowMetrics)
}
val tag = rule.tag
@@ -195,30 +300,103 @@
}
}
+ @OptIn(ExperimentalWindowApi::class)
+ fun translateSplitPinRule(context: Context, splitPinRule: SplitPinRule): OEMSplitPinRule {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+ val windowMetricsPredicate =
+ Predicate2<AndroidWindowMetrics> { windowMetrics ->
+ splitPinRule.checkParentMetrics(context, windowMetrics)
+ }
+ val builder =
+ SplitPinRuleBuilder(
+ translateSplitAttributes(splitPinRule.defaultSplitAttributes),
+ windowMetricsPredicate
+ )
+ builder.setSticky(splitPinRule.isSticky)
+ val tag = splitPinRule.tag
+ if (tag != null) {
+ builder.setTag(tag)
+ }
+ return builder.build()
+ }
+
+ @OptIn(ExperimentalWindowApi::class)
+ @Suppress("DEPRECATION") // To compat with device with extension versions 5 and 6.
fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
- require(vendorApiLevel >= 2)
+ require(extensionVersion >= 2)
// To workaround the "unused" error in ktlint. It is necessary to translate SplitAttributes
// from WM Jetpack version to WM extension version.
- return androidx.window.extensions.embedding.SplitAttributes.Builder()
- .setSplitType(translateSplitType(splitAttributes.splitType))
- .setLayoutDirection(
- when (splitAttributes.layoutDirection) {
- LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
- LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
- RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
- TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
- BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
- else ->
- throw IllegalArgumentException(
- "Unsupported layoutDirection:" + "$splitAttributes.layoutDirection"
- )
- }
+ val builder =
+ OEMSplitAttributes.Builder()
+ .setSplitType(translateSplitType(splitAttributes.splitType))
+ .setLayoutDirection(
+ when (splitAttributes.layoutDirection) {
+ LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
+ LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
+ RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
+ TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+ BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+ else ->
+ throw IllegalArgumentException(
+ "Unsupported layoutDirection:" + "$splitAttributes.layoutDirection"
+ )
+ }
+ )
+ if (extensionVersion >= 5) {
+ builder.setWindowAttributes(translateWindowAttributes())
+ }
+ if (extensionVersion in 5..6) {
+ builder.setAnimationBackground(
+ translateToOemAnimationBackground(
+ splitAttributes.animationParams.animationBackground
+ )
)
- .build()
+ }
+ if (extensionVersion >= 7) {
+ val animationParams =
+ OEMEmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ translateToOemAnimationBackground(
+ splitAttributes.animationParams.animationBackground
+ )
+ )
+ .setOpenAnimationResId(
+ translateToOemAnimationResId(splitAttributes.animationParams.openAnimation)
+ )
+ .setCloseAnimationResId(
+ translateToOemAnimationResId(splitAttributes.animationParams.closeAnimation)
+ )
+ .setChangeAnimationResId(
+ translateToOemAnimationResId(
+ splitAttributes.animationParams.changeAnimation
+ )
+ )
+ .build()
+ builder.setAnimationParams(animationParams)
+ }
+ if (extensionVersion >= 6) {
+ builder.setDividerAttributes(
+ translateToOemDividerAttributes(splitAttributes.dividerAttributes)
+ )
+ }
+ return builder.build()
+ }
+
+ /** Translates [embeddingConfiguration] from adapter to [WindowAttributes]. */
+ @OptIn(ExperimentalWindowApi::class)
+ internal fun translateWindowAttributes(): OEMWindowAttributes = let {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+
+ OEMWindowAttributes(
+ when (embeddingConfiguration?.dimAreaBehavior) {
+ ON_ACTIVITY_STACK -> OEMWindowAttributes.DIM_AREA_ON_ACTIVITY_STACK
+ else -> OEMWindowAttributes.DIM_AREA_ON_TASK
+ }
+ )
}
private fun translateSplitType(splitType: SplitType): OEMSplitType {
- require(vendorApiLevel >= 2)
+ require(extensionVersion >= 2)
return when (splitType) {
SPLIT_TYPE_HINGE -> OEMSplitType.HingeSplitType(translateSplitType(SPLIT_TYPE_EQUAL))
SPLIT_TYPE_EXPAND -> OEMSplitType.ExpandContainersSplitType()
@@ -240,7 +418,7 @@
rule: SplitPlaceholderRule,
predicateClass: Class<*>
): OEMSplitPlaceholderRule {
- if (vendorApiLevel < 2) {
+ if (extensionVersion < 2) {
return api1Impl.translateSplitPlaceholderRuleCompat(context, rule, predicateClass)
} else {
val activityPredicate =
@@ -252,7 +430,7 @@
rule.filters.any { filter -> filter.matchesIntent(intent) }
}
val windowMetricsPredicate =
- Predicate2<WindowMetrics> { windowMetrics ->
+ Predicate2<AndroidWindowMetrics> { windowMetrics ->
rule.checkParentMetrics(context, windowMetrics)
}
val tag = rule.tag
@@ -289,7 +467,7 @@
rule: ActivityRule,
predicateClass: Class<*>
): OEMActivityRule {
- if (vendorApiLevel < 2) {
+ if (extensionVersion < 2) {
return api1Impl.translateActivityRuleCompat(rule, predicateClass)
} else {
val activityPredicate =
@@ -326,29 +504,160 @@
.toSet()
}
+ @RequiresWindowSdkExtension(5)
+ private fun translateToOemAnimationBackground(
+ animationBackground: EmbeddingAnimationBackground
+ ): OEMEmbeddingAnimationBackground {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+ return if (animationBackground is EmbeddingAnimationBackground.ColorBackground) {
+ OEMEmbeddingAnimationBackground.createColorBackground(animationBackground.color)
+ } else {
+ OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+ }
+ }
+
+ @RequiresWindowSdkExtension(5)
+ private fun translateToJetpackAnimationBackground(
+ animationBackground: OEMEmbeddingAnimationBackground
+ ): EmbeddingAnimationBackground {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+ return if (animationBackground is OEMEmbeddingAnimationBackground.ColorBackground) {
+ EmbeddingAnimationBackground.createColorBackground(animationBackground.color)
+ } else {
+ EmbeddingAnimationBackground.DEFAULT
+ }
+ }
+
+ @RequiresWindowSdkExtension(7)
+ private fun translateToOemAnimationResId(
+ animationSpec: EmbeddingAnimationParams.AnimationSpec
+ ): Int {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(7)
+ return if (animationSpec == EmbeddingAnimationParams.AnimationSpec.JUMP_CUT) {
+ Resources.ID_NULL
+ } else {
+ OEMEmbeddingAnimationParams.DEFAULT_ANIMATION_RESOURCES_ID
+ }
+ }
+
+ @RequiresWindowSdkExtension(7)
+ private fun translateToJetpackAnimationSpec(
+ animationResId: Int
+ ): EmbeddingAnimationParams.AnimationSpec {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(7)
+ return if (animationResId == Resources.ID_NULL) {
+ EmbeddingAnimationParams.AnimationSpec.JUMP_CUT
+ } else {
+ EmbeddingAnimationParams.AnimationSpec.DEFAULT
+ }
+ }
+
+ @RequiresWindowSdkExtension(6)
+ fun translateToOemDividerAttributes(
+ dividerAttributes: DividerAttributes
+ ): OEMDividerAttributes? {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+ if (dividerAttributes === DividerAttributes.NO_DIVIDER) {
+ return null
+ }
+ val builder =
+ OEMDividerAttributes.Builder(
+ when (dividerAttributes) {
+ is FixedDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_FIXED
+ is DraggableDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE
+ else ->
+ throw IllegalArgumentException(
+ "Unknown divider attributes $dividerAttributes"
+ )
+ }
+ )
+ .setDividerColor(dividerAttributes.color)
+ .setWidthDp(dividerAttributes.widthDp)
+ if (dividerAttributes is DraggableDividerAttributes) {
+ if (dividerAttributes.dragRange is SplitRatioDragRange) {
+ builder
+ .setPrimaryMinRatio(dividerAttributes.dragRange.minRatio)
+ .setPrimaryMaxRatio(dividerAttributes.dragRange.maxRatio)
+ }
+ if (extensionVersion >= 7) {
+ builder.setDraggingToFullscreenAllowed(
+ dividerAttributes.isDraggingToFullscreenAllowed
+ )
+ }
+ }
+ return builder.build()
+ }
+
+ @RequiresWindowSdkExtension(6)
+ fun translateToJetpackDividerAttributes(
+ oemDividerAttributes: OEMDividerAttributes?
+ ): DividerAttributes {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+ if (oemDividerAttributes == null) {
+ return DividerAttributes.NO_DIVIDER
+ }
+ return when (oemDividerAttributes.dividerType) {
+ OEMDividerAttributes.DIVIDER_TYPE_FIXED ->
+ FixedDividerAttributes.Builder()
+ .setWidthDp(oemDividerAttributes.widthDp)
+ .setColor(oemDividerAttributes.dividerColor)
+ .build()
+ OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE ->
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(oemDividerAttributes.widthDp)
+ .setColor(oemDividerAttributes.dividerColor)
+ .setDragRange(
+ if (
+ oemDividerAttributes.primaryMinRatio == RATIO_SYSTEM_DEFAULT &&
+ oemDividerAttributes.primaryMaxRatio == RATIO_SYSTEM_DEFAULT
+ )
+ DRAG_RANGE_SYSTEM_DEFAULT
+ else
+ SplitRatioDragRange(
+ oemDividerAttributes.primaryMinRatio,
+ oemDividerAttributes.primaryMaxRatio,
+ )
+ )
+ .setDraggingToFullscreenAllowed(
+ extensionVersion >= 7 && oemDividerAttributes.isDraggingToFullscreenAllowed
+ )
+ .build()
+ // Default to DividerType.FIXED
+ else -> {
+ Log.w(
+ TAG,
+ "Unknown divider type $oemDividerAttributes.dividerType, default" +
+ " to fixed divider type"
+ )
+ FixedDividerAttributes.Builder()
+ .setWidthDp(oemDividerAttributes.widthDp)
+ .setColor(oemDividerAttributes.dividerColor)
+ .build()
+ }
+ }
+ }
+
+ /** Provides backward compatibility for Window extensions with API level 3 */
+ // Suppress deprecation because this object is to provide backward compatibility.
+ @Suppress("DEPRECATION")
+ private inner class VendorApiLevel3Impl {
+ fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
+ SplitInfo(
+ api1Impl.translateCompat(splitInfo.primaryActivityStack),
+ api1Impl.translateCompat(splitInfo.secondaryActivityStack),
+ translate(splitInfo.splitAttributes),
+ splitInfo.token,
+ )
+ }
+
/** Provides backward compatibility for Window extensions with API level 2 */
private inner class VendorApiLevel2Impl {
- fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo {
- val primaryActivityStack = splitInfo.primaryActivityStack
- val primaryFragment =
- ActivityStack(
- primaryActivityStack.activities,
- primaryActivityStack.isEmpty,
- )
-
- val secondaryActivityStack = splitInfo.secondaryActivityStack
- val secondaryFragment =
- ActivityStack(
- secondaryActivityStack.activities,
- secondaryActivityStack.isEmpty,
- )
- return SplitInfo(
- primaryFragment,
- secondaryFragment,
+ fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
+ SplitInfo(
+ api1Impl.translateCompat(splitInfo.primaryActivityStack),
+ api1Impl.translateCompat(splitInfo.secondaryActivityStack),
translate(splitInfo.splitAttributes),
- INVALID_SPLIT_INFO_TOKEN,
)
- }
}
/** Provides backward compatibility for [WindowSdkExtensions] version 1 */
@@ -508,35 +817,31 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
- predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
+ predicateAdapter.buildPredicate(AndroidWindowMetrics::class) { windowMetrics ->
splitRule.checkParentMetrics(context, windowMetrics)
}
fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
SplitInfo(
- ActivityStack(
- splitInfo.primaryActivityStack.activities,
- splitInfo.primaryActivityStack.isEmpty,
- ),
- ActivityStack(
- splitInfo.secondaryActivityStack.activities,
- splitInfo.secondaryActivityStack.isEmpty,
- ),
+ translateCompat(splitInfo.primaryActivityStack),
+ translateCompat(splitInfo.secondaryActivityStack),
getSplitAttributesCompat(splitInfo),
- INVALID_SPLIT_INFO_TOKEN,
+ )
+
+ fun translateCompat(activityStack: OEMActivityStack): ActivityStack =
+ ActivityStack(
+ activityStack.activities,
+ activityStack.isEmpty,
)
}
internal companion object {
+ private val TAG = EmbeddingAdapter::class.simpleName
+
/**
* The default token of [SplitInfo], which provides compatibility for device prior to vendor
* API level 3
*/
val INVALID_SPLIT_INFO_TOKEN = Binder()
- /**
- * The default token of [ActivityStack], which provides compatibility for device prior to
- * vendor API level 3
- */
- val INVALID_ACTIVITY_STACK_TOKEN = Binder()
}
}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
new file mode 100644
index 0000000..64a354f
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+
+/**
+ * Background to be used for window transition animations for embedding activities if the animation
+ * requires a background.
+ *
+ * @see EmbeddingAnimationParams.animationBackground
+ */
+abstract class EmbeddingAnimationBackground private constructor() {
+
+ /**
+ * An {@link EmbeddingAnimationBackground} to specify of using a developer-defined color as the
+ * animation background. Only opaque background is supported.
+ *
+ * @see EmbeddingAnimationBackground.createColorBackground
+ */
+ class ColorBackground
+ internal constructor(
+ /** [ColorInt] to represent the color to use as the background color. */
+ @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong()) @ColorInt val color: Int
+ ) : EmbeddingAnimationBackground() {
+
+ init {
+ require(Color.alpha(color) == 255) { "Background color must be opaque" }
+ }
+
+ override fun toString() = "ColorBackground{color:${Integer.toHexString(color)}}"
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other !is ColorBackground) return false
+ return color == other.color
+ }
+
+ override fun hashCode() = color.hashCode()
+ }
+
+ /** @see EmbeddingAnimationBackground.DEFAULT */
+ private class DefaultBackground : EmbeddingAnimationBackground() {
+
+ override fun toString() = "DefaultBackground"
+ }
+
+ /** Methods that create various [EmbeddingAnimationBackground]. */
+ companion object {
+
+ /**
+ * Creates a [ColorBackground] to represent the given [color].
+ *
+ * Only opaque color is supported.
+ *
+ * @param color [ColorInt] of an opaque color.
+ * @return the [ColorBackground] representing the [color].
+ * @throws IllegalArgumentException if the [color] is not opaque.
+ * @see [DEFAULT] for the default value, which means to use the current theme window
+ * background color.
+ */
+ @JvmStatic
+ fun createColorBackground(
+ @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong()) @ColorInt color: Int
+ ): ColorBackground = ColorBackground(color)
+
+ /**
+ * The special [EmbeddingAnimationBackground] to represent the default value, which means to
+ * use the current theme window background color.
+ */
+ @JvmField val DEFAULT: EmbeddingAnimationBackground = DefaultBackground()
+
+ /** Returns an [EmbeddingAnimationBackground] with the given [color] */
+ internal fun buildFromValue(@ColorInt color: Int): EmbeddingAnimationBackground {
+ return if (Color.alpha(color) != 255) {
+ // Treat any non-opaque color as the default.
+ DEFAULT
+ } else {
+ createColorBackground(color)
+ }
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt
new file mode 100644
index 0000000..7473e49
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2024 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion.DEFAULT
+
+/**
+ * Parameters to be used for window transition animations for embedding activities.
+ *
+ * @property animationBackground the animation background to use during the animation of the split
+ * involving this `EmbeddingAnimationParams` object if the animation requires a background. The
+ * default is to use the current theme window background color.
+ * @property openAnimation the animation spec to use for open transitions (when starting/entering an
+ * activity or when an activity moves to front).
+ * @property closeAnimation the animation spec to use for close transitions (when finishing/exiting
+ * an activity or when an activity moves to back).
+ * @property changeAnimation the animation spec to use for change transitions (when an activity
+ * resizes or moves).
+ * @see SplitAttributes.animationParams
+ * @see EmbeddingAnimationBackground
+ * @see EmbeddingAnimationBackground.createColorBackground
+ * @see EmbeddingAnimationBackground.DEFAULT
+ */
+class EmbeddingAnimationParams
+@JvmOverloads
+constructor(
+ val animationBackground: EmbeddingAnimationBackground = EmbeddingAnimationBackground.DEFAULT,
+ val openAnimation: AnimationSpec = DEFAULT,
+ val closeAnimation: AnimationSpec = DEFAULT,
+ val changeAnimation: AnimationSpec = DEFAULT,
+) {
+
+ /** The animation to use when an activity transitions (e.g. open, close, or change). */
+ class AnimationSpec
+ private constructor(
+ /**
+ * The unique integer value for the `splitAnimationSpec`. This can be used as an enum value
+ * when defining `splitAnimationSpec` attributes in XML.
+ */
+ internal val value: Int,
+ ) {
+
+ /**
+ * A string representation of this `AnimationSpec`.
+ *
+ * @return the string representation of the object.
+ */
+ override fun toString(): String =
+ when (value) {
+ 0 -> "DEFAULT"
+ 1 -> "JUMP_CUT"
+ else -> "Unknown value: $value"
+ }
+
+ /** Properties and methods. */
+ companion object {
+ /** Specifies the default animation defined by the system. */
+ @JvmField val DEFAULT = AnimationSpec(0)
+ /** Specifies an animation with zero duration. */
+ @JvmField val JUMP_CUT = AnimationSpec(1)
+
+ /** Returns `AnimationSpec` with the given integer `value`. */
+ @JvmStatic
+ internal fun getAnimationSpecFromValue(@IntRange(from = 0, to = 1) value: Int) =
+ when (value) {
+ DEFAULT.value -> DEFAULT
+ JUMP_CUT.value -> JUMP_CUT
+ else -> throw IllegalArgumentException("Undefined value:$value")
+ }
+ }
+ }
+
+ /**
+ * Returns a hash code for this `EmbeddingAnimationParams` object.
+ *
+ * @return the hash code for this object.
+ */
+ override fun hashCode(): Int {
+ var result = animationBackground.hashCode()
+ result = result * 31 + openAnimation.hashCode()
+ result = result * 31 + closeAnimation.hashCode()
+ result = result * 31 + changeAnimation.hashCode()
+ return result
+ }
+
+ /**
+ * Determines whether this object has the same animation parameters as the compared object.
+ *
+ * @param other the object to compare to this object.
+ * @return true if the objects have the same animation parameters, false otherwise.
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EmbeddingAnimationParams) return false
+ return animationBackground == other.animationBackground &&
+ openAnimation == other.openAnimation &&
+ closeAnimation == other.closeAnimation &&
+ changeAnimation == other.changeAnimation
+ }
+
+ /**
+ * A string representation of this `EmbeddingAnimationParams` object.
+ *
+ * @return the string representation of the object.
+ */
+ override fun toString(): String =
+ "${EmbeddingAnimationParams::class.java.simpleName}:" +
+ "{animationBackground=$animationBackground, openAnimation=$openAnimation, " +
+ "closeAnimation=$closeAnimation, changeAnimation=$changeAnimation }"
+
+ /** Builder for creating an instance of [EmbeddingAnimationParams]. */
+ class Builder {
+ private var animationBackground = EmbeddingAnimationBackground.DEFAULT
+ private var openAnimation = DEFAULT
+ private var closeAnimation = DEFAULT
+ private var changeAnimation = DEFAULT
+
+ /**
+ * Sets the animation background.
+ *
+ * The default is to use the current theme window background color.
+ *
+ * @param background the animation background.
+ * @return this `Builder`.
+ * @see EmbeddingAnimationBackground
+ */
+ fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder = apply {
+ this.animationBackground = background
+ }
+
+ /**
+ * Sets the open animation.
+ *
+ * The default is to use the system animation.
+ *
+ * @param spec the animation transition spec
+ * @return this `Builder`.
+ */
+ fun setOpenAnimation(spec: AnimationSpec): Builder = apply { this.openAnimation = spec }
+
+ /**
+ * Sets the close animation.
+ *
+ * The default is to use the system animation.
+ *
+ * @param spec the animation transition spec
+ * @return this `Builder`.
+ */
+ fun setCloseAnimation(spec: AnimationSpec): Builder = apply { this.closeAnimation = spec }
+
+ /**
+ * Sets the change (resize or move) animation.
+ *
+ * The default is to use the system animation.
+ *
+ * @param spec the animation spec
+ * @return this `Builder`.
+ */
+ fun setChangeAnimation(spec: AnimationSpec): Builder = apply { this.changeAnimation = spec }
+
+ /**
+ * Builds an `EmbeddingAnimationParams` instance with the attributes specified by the
+ * builder's setters.
+ *
+ * @return the new `EmbeddingAnimationParams` instance.
+ */
+ fun build(): EmbeddingAnimationParams =
+ EmbeddingAnimationParams(
+ animationBackground,
+ openAnimation,
+ closeAnimation,
+ changeAnimation
+ )
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index 2711813..a66b290 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -17,12 +17,12 @@
package androidx.window.embedding
import android.app.Activity
-import android.app.ActivityOptions
import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.core.util.Consumer
import androidx.window.RequiresWindowSdkExtension
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import java.util.concurrent.Executor
/** */
@@ -48,6 +48,11 @@
fun isActivityEmbedded(activity: Activity): Boolean
+ @RequiresWindowSdkExtension(5)
+ fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
+
+ @RequiresWindowSdkExtension(5) fun unpinTopActivityStack(taskId: Int)
+
@RequiresWindowSdkExtension(2)
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -57,14 +62,53 @@
fun getActivityStack(activity: Activity): ActivityStack?
- @RequiresWindowSdkExtension(3)
- fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+ @RequiresWindowSdkExtension(5)
+ fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
- @RequiresWindowSdkExtension(3) fun invalidateTopVisibleSplitAttributes()
+ @RequiresWindowSdkExtension(5)
+ fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
+
+ @RequiresWindowSdkExtension(5) fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+ @RequiresWindowSdkExtension(5)
+ fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
+
+ @RequiresWindowSdkExtension(3) fun invalidateVisibleActivityStacks()
@RequiresWindowSdkExtension(3)
fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ )
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION) fun clearOverlayAttributesCalculator()
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: Consumer<OverlayInfo>,
+ )
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>)
+
+ @RequiresWindowSdkExtension(6)
+ fun addEmbeddedActivityWindowInfoCallbackForActivity(
+ activity: Activity,
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ )
+
+ @RequiresWindowSdkExtension(6)
+ fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ )
+
companion object {
private var decorator: (EmbeddingBackend) -> EmbeddingBackend = { it }
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
new file mode 100644
index 0000000..42ef668
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.graphics.Rect
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.annotation.Px
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.window.core.Bounds
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import kotlin.math.min
+
+/**
+ * The bounds of a standalone [ActivityStack].
+ *
+ * It can be either described with `alignment`, `width` and `height` or predefined constant values.
+ * Some important constants are:
+ * - [BOUNDS_EXPANDED]: To indicate the bounds fills the parent window container.
+ * - [BOUNDS_HINGE_TOP]: To indicate the bounds are at the top of the parent window container while
+ * its bottom follows the hinge position. Refer to [BOUNDS_HINGE_LEFT], [BOUNDS_HINGE_BOTTOM] and
+ * [BOUNDS_HINGE_RIGHT] for other bounds that follows the hinge position.
+ *
+ * @constructor creates an embedding bounds.
+ * @property alignment The alignment of the bounds relative to parent window container.
+ * @property width The width of the bounds.
+ * @property height The height of the bounds.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class EmbeddingBounds(val alignment: Alignment, val width: Dimension, val height: Dimension) {
+ override fun toString(): String {
+ return "Bounds:{alignment=$alignment, width=$width, height=$height}"
+ }
+
+ override fun hashCode(): Int {
+ var result = alignment.hashCode()
+ result = result * 31 + width.hashCode()
+ result = result * 31 + height.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EmbeddingBounds) return false
+ return alignment == other.alignment && width == other.width && height == other.height
+ }
+
+ /** Returns `true` if the [width] should fallback to half of parent task width. */
+ internal fun shouldUseFallbackDimensionForWidth(windowLayoutInfo: WindowLayoutInfo): Boolean {
+ if (width != DIMENSION_HINGE) {
+ return false
+ }
+ return !windowLayoutInfo.isVertical() || alignment in listOf(ALIGN_TOP, ALIGN_BOTTOM)
+ }
+
+ /** Returns `true` if the [height] should fallback to half of parent task height. */
+ internal fun shouldUseFallbackDimensionForHeight(windowLayoutInfo: WindowLayoutInfo): Boolean {
+ if (height != DIMENSION_HINGE) {
+ return false
+ }
+ return !windowLayoutInfo.isHorizontal() || alignment in listOf(ALIGN_LEFT, ALIGN_RIGHT)
+ }
+
+ private fun WindowLayoutInfo.isHorizontal(): Boolean {
+ val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
+ return foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+ }
+
+ private fun WindowLayoutInfo.isVertical(): Boolean {
+ val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
+ return foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
+ }
+
+ /**
+ * Returns [FoldingFeature] if it's the only `FoldingFeature` in [WindowLayoutInfo]. Returns
+ * `null`, otherwise.
+ */
+ private fun WindowLayoutInfo.getOnlyFoldingFeatureOrNull(): FoldingFeature? {
+ val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
+ return if (foldingFeatures.size == 1) foldingFeatures[0] else null
+ }
+
+ /** Calculates [width] in pixel with [parentContainerBounds] and [windowLayoutInfo]. */
+ @Px
+ internal fun getWidthInPixel(
+ parentContainerBounds: Bounds,
+ windowLayoutInfo: WindowLayoutInfo
+ ): Int {
+ val taskWidth = parentContainerBounds.width
+ val widthDimension =
+ if (shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
+ ratio(0.5f)
+ } else {
+ width
+ }
+ when (widthDimension) {
+ is Dimension.Ratio -> return widthDimension * taskWidth
+ is Dimension.Pixel -> return min(taskWidth, widthDimension.value)
+ DIMENSION_HINGE -> {
+ // Should be verified by #shouldUseFallbackDimensionForWidth
+ val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
+ return when (alignment) {
+ ALIGN_LEFT -> {
+ hingeBounds.left - parentContainerBounds.left
+ }
+ ALIGN_RIGHT -> {
+ parentContainerBounds.right - hingeBounds.right
+ }
+ else -> {
+ throw IllegalStateException(
+ "Unhandled condition to get height in pixel! " +
+ "embeddingBounds=$this taskBounds=$parentContainerBounds " +
+ "windowLayoutInfo=$windowLayoutInfo"
+ )
+ }
+ }
+ }
+ else -> throw IllegalArgumentException("Unhandled width dimension=$width")
+ }
+ }
+
+ /** Calculates [height] in pixel with [parentContainerBounds] and [windowLayoutInfo]. */
+ @Px
+ internal fun getHeightInPixel(
+ parentContainerBounds: Bounds,
+ windowLayoutInfo: WindowLayoutInfo
+ ): Int {
+ val taskHeight = parentContainerBounds.height
+ val heightDimension =
+ if (shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {
+ ratio(0.5f)
+ } else {
+ height
+ }
+ when (heightDimension) {
+ is Dimension.Ratio -> return heightDimension * taskHeight
+ is Dimension.Pixel -> return min(taskHeight, heightDimension.value)
+ DIMENSION_HINGE -> {
+ // Should be verified by #shouldUseFallbackDimensionForWidth
+ val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
+ return when (alignment) {
+ ALIGN_TOP -> {
+ hingeBounds.top - parentContainerBounds.top
+ }
+ ALIGN_BOTTOM -> {
+ parentContainerBounds.bottom - hingeBounds.bottom
+ }
+ else -> {
+ throw IllegalStateException(
+ "Unhandled condition to get height in pixel! " +
+ "embeddingBounds=$this taskBounds=$parentContainerBounds " +
+ "windowLayoutInfo=$windowLayoutInfo"
+ )
+ }
+ }
+ }
+ else -> throw IllegalArgumentException("Unhandled width dimension=$width")
+ }
+ }
+
+ /** The position of the bounds relative to parent window container. */
+ class Alignment internal constructor(@IntRange(from = 0, to = 3) internal val value: Int) {
+
+ init {
+ require(value in 0..3)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Alignment) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return value
+ }
+
+ override fun toString(): String =
+ when (value) {
+ 0 -> "left"
+ 1 -> "top"
+ 2 -> "right"
+ 3 -> "bottom"
+ else -> "unknown position:$value"
+ }
+
+ companion object {
+
+ /** Specifies that the bounds is at the left of the parent window container. */
+ @JvmField val ALIGN_LEFT = Alignment(0)
+
+ /** Specifies that the bounds is at the top of the parent window container. */
+ @JvmField val ALIGN_TOP = Alignment(1)
+
+ /** Specifies that the bounds is at the right of the parent window container. */
+ @JvmField val ALIGN_RIGHT = Alignment(2)
+
+ /** Specifies that the bounds is at the bottom of the parent window container. */
+ @JvmField val ALIGN_BOTTOM = Alignment(3)
+ }
+ }
+
+ /**
+ * The dimension of the bounds, which can be represented as multiple formats:
+ * - [DIMENSION_EXPANDED]: means the bounds' dimension fills parent window's dimension.
+ * - in [pixel]: To specify the dimension value in pixel.
+ * - in [ratio]: To specify the dimension that relative to the parent window container. For
+ * example, if [width] has [ratio] value 0.6, it means the bounds' width is 0.6 to the parent
+ * window container's width.
+ */
+ abstract class Dimension internal constructor(internal val description: String) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Dimension) return false
+ return description == other.description
+ }
+
+ override fun hashCode(): Int = description.hashCode()
+
+ override fun toString(): String = description
+
+ /**
+ * The [Dimension] represented in pixel format
+ *
+ * @param value The dimension length in pixel
+ */
+ internal class Pixel(@Px @IntRange(from = 1) internal val value: Int) :
+ Dimension("dimension in pixel:$value") {
+
+ init {
+ require(value >= 1) { "Pixel value must be a positive integer." }
+ }
+
+ internal operator fun compareTo(dimen: Int): Int = value - dimen
+ }
+
+ /**
+ * The [Dimension] represented in ratio format, which means the proportion of the parent
+ * window dimension.
+ *
+ * @param value The ratio in (0.0, 1.0)
+ */
+ internal class Ratio(
+ @FloatRange(from = 0.0, fromInclusive = false, to = 1.0) internal val value: Float
+ ) : Dimension("dimension in ratio:$value") {
+
+ init {
+ require(value > 0.0 && value <= 1.0) { "Ratio must be in range (0.0, 1.0]" }
+ }
+
+ internal operator fun times(dimen: Int): Int = (value * dimen).toInt()
+ }
+
+ companion object {
+
+ /** Represents this dimension follows its parent window dimension. */
+ @JvmField val DIMENSION_EXPANDED: Dimension = Ratio(1.0f)
+
+ /**
+ * Represents this dimension follows the hinge position if the current window and device
+ * state satisfies, or fallbacks to a half of the parent task dimension, otherwise.
+ *
+ * The [DIMENSION_HINGE] works only if:
+ * - The parent container is not in multi-window mode (e.g., split-screen mode or
+ * picture-in-picture mode)
+ * - The device has a hinge or separating fold reported by
+ * [androidx.window.layout.FoldingFeature.isSeparating]
+ * - The hinge or separating fold orientation matches [EmbeddingBounds.alignment]:
+ * - The hinge or fold orientation is vertical, and the position is [POSITION_LEFT]
+ * or [POSITION_RIGHT]
+ * - The hinge or fold orientation is horizontal, and the position is [POSITION_TOP]
+ * or [POSITION_BOTTOM]
+ */
+ @JvmField val DIMENSION_HINGE: Dimension = object : Dimension("hinge") {}
+
+ /**
+ * Creates the dimension in pixel.
+ *
+ * If the dimension length exceeds the parent window dimension, the overlay container
+ * will resize to fit the parent task dimension.
+ *
+ * @param value The dimension length in pixel
+ */
+ @JvmStatic fun pixel(@Px @IntRange(from = 1) value: Int): Dimension = Pixel(value)
+
+ /**
+ * Creates the dimension which takes a proportion of the parent window dimension.
+ *
+ * @param ratio The proportion of the parent window dimension this dimension should take
+ */
+ @JvmStatic
+ fun ratio(
+ @FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = false)
+ ratio: Float
+ ): Dimension = Ratio(ratio)
+ }
+ }
+
+ companion object {
+
+ /** The bounds fills the parent window bounds */
+ @JvmField
+ val BOUNDS_EXPANDED = EmbeddingBounds(ALIGN_TOP, DIMENSION_EXPANDED, DIMENSION_EXPANDED)
+
+ /**
+ * The bounds located on the top of the parent window, and the bounds' bottom side matches
+ * the hinge position.
+ */
+ @JvmField
+ val BOUNDS_HINGE_TOP =
+ EmbeddingBounds(ALIGN_TOP, width = DIMENSION_EXPANDED, height = DIMENSION_HINGE)
+
+ /**
+ * The bounds located on the left of the parent window, and the bounds' right side matches
+ * the hinge position.
+ */
+ @JvmField
+ val BOUNDS_HINGE_LEFT =
+ EmbeddingBounds(ALIGN_LEFT, width = DIMENSION_HINGE, height = DIMENSION_EXPANDED)
+
+ /**
+ * The bounds located on the bottom of the parent window, and the bounds' top side matches
+ * the hinge position.
+ */
+ @JvmField
+ val BOUNDS_HINGE_BOTTOM =
+ EmbeddingBounds(ALIGN_BOTTOM, width = DIMENSION_EXPANDED, height = DIMENSION_HINGE)
+
+ /**
+ * The bounds located on the right of the parent window, and the bounds' left side matches
+ * the hinge position.
+ */
+ @JvmField
+ val BOUNDS_HINGE_RIGHT =
+ EmbeddingBounds(ALIGN_RIGHT, width = DIMENSION_HINGE, height = DIMENSION_EXPANDED)
+
+ /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
+ @VisibleForTesting
+ internal fun translateEmbeddingBounds(
+ embeddingBounds: EmbeddingBounds,
+ parentContainerBounds: Bounds,
+ windowLayoutInfo: WindowLayoutInfo,
+ ): Bounds {
+ if (
+ embeddingBounds.width == DIMENSION_EXPANDED &&
+ embeddingBounds.height == DIMENSION_EXPANDED
+ ) {
+ // If width and height are expanded, set bounds to empty to follow the parent task
+ // bounds.
+ return Bounds.EMPTY_BOUNDS
+ }
+ // 1. Fallbacks dimensions to ratio(0.5) if they can't follow the hinge with the current
+ // device and window state.
+ val width =
+ if (embeddingBounds.shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
+ ratio(0.5f)
+ } else {
+ embeddingBounds.width
+ }
+ val height =
+ if (embeddingBounds.shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {
+ ratio(0.5f)
+ } else {
+ embeddingBounds.height
+ }
+
+ // 2. Computes dimensions to pixel values. If it just matches parent task bounds,
+ // returns
+ // the empty bounds to declare the bounds follow the parent task bounds.
+ val sanitizedBounds = EmbeddingBounds(embeddingBounds.alignment, width, height)
+ val widthInPixel =
+ sanitizedBounds.getWidthInPixel(parentContainerBounds, windowLayoutInfo)
+ val heightInPixel =
+ sanitizedBounds.getHeightInPixel(parentContainerBounds, windowLayoutInfo)
+ val taskWidth = parentContainerBounds.width
+ val taskHeight = parentContainerBounds.height
+
+ if (widthInPixel == taskWidth && heightInPixel == taskHeight) {
+ return Bounds.EMPTY_BOUNDS
+ }
+
+ // 3. Offset the bounds by position:
+ // - For top or bottom position, the bounds should attach to the top or bottom of
+ // the parent task bounds and centered by the middle of the width.
+ // - For left or right position, the bounds should attach to the left or right of
+ // the parent task bounds and centered by the middle of the height.
+ return Bounds(0, 0, widthInPixel, heightInPixel).let { bounds ->
+ when (embeddingBounds.alignment) {
+ ALIGN_TOP -> bounds.offset(((taskWidth - widthInPixel) / 2), 0)
+ ALIGN_LEFT -> bounds.offset(0, ((taskHeight - heightInPixel) / 2))
+ ALIGN_BOTTOM ->
+ bounds.offset(((taskWidth - widthInPixel) / 2), taskHeight - heightInPixel)
+ ALIGN_RIGHT ->
+ bounds.offset(taskWidth - widthInPixel, ((taskHeight - heightInPixel) / 2))
+ else ->
+ throw IllegalArgumentException(
+ "Unknown alignment: ${embeddingBounds.alignment}"
+ )
+ }
+ }
+ }
+
+ private fun Bounds.offset(dx: Int, dy: Int): Bounds =
+ Bounds(left + dx, top + dy, right + dx, bottom + dy)
+
+ /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
+ internal fun translateEmbeddingBounds(
+ embeddingBounds: EmbeddingBounds,
+ parentContainerInfo: ParentContainerInfo,
+ ): Bounds =
+ translateEmbeddingBounds(
+ embeddingBounds,
+ parentContainerInfo.windowBounds,
+ parentContainerInfo.windowLayoutInfo
+ )
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index fd35178..e982f91 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -17,23 +17,27 @@
package androidx.window.embedding
import android.app.Activity
-import android.app.ActivityOptions
import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
import androidx.window.core.BuildConfig
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import androidx.window.reflection.Consumer2
import java.lang.reflect.Proxy
+import java.util.concurrent.Executor
/**
* Adapter implementation for different historical versions of activity embedding OEM interface in
@@ -43,9 +47,15 @@
private val embeddingExtension: ActivityEmbeddingComponent,
private val adapter: EmbeddingAdapter,
private val consumerAdapter: ConsumerAdapter,
- private val applicationContext: Context
+ private val applicationContext: Context,
+ @get:VisibleForTesting internal val overlayController: OverlayControllerImpl?,
+ private val activityWindowInfoCallbackController: ActivityWindowInfoCallbackController?,
) : EmbeddingInterfaceCompat {
+ private val windowSdkExtensions = WindowSdkExtensions.getInstance()
+
+ private var isCustomSplitAttributeCalculatorSet: Boolean = false
+
override fun setRules(rules: Set<EmbeddingRule>) {
var hasSplitRule = false
for (rule in rules) {
@@ -74,70 +84,238 @@
}
override fun setEmbeddingCallback(embeddingCallback: EmbeddingCallbackInterface) {
- if (ExtensionsUtil.safeVendorApiLevel < 2) {
- consumerAdapter.addConsumer(embeddingExtension, List::class, "setSplitInfoCallback") {
- values ->
- val splitInfoList = values.filterIsInstance<OEMSplitInfo>()
- embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
- }
- } else {
- val callback =
- Consumer2<List<OEMSplitInfo>> { splitInfoList ->
+ when (windowSdkExtensions.extensionVersion) {
+ 1 -> {
+ consumerAdapter.addConsumer(
+ embeddingExtension,
+ List::class,
+ "setSplitInfoCallback"
+ ) { values ->
+ val splitInfoList = values.filterIsInstance<OEMSplitInfo>()
embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
}
- embeddingExtension.setSplitInfoCallback(callback)
+ }
+ in 2..4 -> {
+ registerSplitInfoCallback(embeddingCallback)
+ }
+ in 5..Int.MAX_VALUE -> {
+ registerSplitInfoCallback(embeddingCallback)
+
+ // Register ActivityStack callback
+ val activityStackCallback =
+ Consumer2<List<OEMActivityStack>> { activityStacks ->
+ embeddingCallback.onActivityStackChanged(adapter.translate(activityStacks))
+ }
+ embeddingExtension.registerActivityStackCallback(
+ Runnable::run,
+ activityStackCallback
+ )
+ }
}
}
+ private fun registerSplitInfoCallback(embeddingCallback: EmbeddingCallbackInterface) {
+ val splitInfoCallback =
+ Consumer2<List<OEMSplitInfo>> { splitInfoList ->
+ embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
+ }
+ embeddingExtension.setSplitInfoCallback(splitInfoCallback)
+ }
+
override fun isActivityEmbedded(activity: Activity): Boolean {
return embeddingExtension.isActivityEmbedded(activity)
}
+ @RequiresWindowSdkExtension(5)
+ override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+ windowSdkExtensions.requireExtensionVersion(5)
+ return embeddingExtension.pinTopActivityStack(
+ taskId,
+ adapter.translateSplitPinRule(applicationContext, splitPinRule)
+ )
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun unpinTopActivityStack(taskId: Int) {
+ windowSdkExtensions.requireExtensionVersion(5)
+ return embeddingExtension.unpinTopActivityStack(taskId)
+ }
+
@RequiresWindowSdkExtension(2)
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
- WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+ windowSdkExtensions.requireExtensionVersion(2)
embeddingExtension.setSplitAttributesCalculator(
adapter.translateSplitAttributesCalculator(calculator)
)
+ isCustomSplitAttributeCalculatorSet = true
}
@RequiresWindowSdkExtension(2)
override fun clearSplitAttributesCalculator() {
- WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+ windowSdkExtensions.requireExtensionVersion(2)
embeddingExtension.clearSplitAttributesCalculator()
+ isCustomSplitAttributeCalculatorSet = false
+ setDefaultSplitAttributeCalculatorIfNeeded()
}
- @RequiresWindowSdkExtension(3)
- override fun invalidateTopVisibleSplitAttributes() {
- WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+ @RequiresWindowSdkExtension(5)
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ windowSdkExtensions.requireExtensionVersion(5)
+
+ embeddingExtension.finishActivityStacksWithTokens(
+ activityStacks.mapTo(mutableSetOf()) { it.getToken() }
+ )
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+ windowSdkExtensions.requireExtensionVersion(5)
+ adapter.embeddingConfiguration = embeddingConfig
+ setDefaultSplitAttributeCalculatorIfNeeded()
embeddingExtension.invalidateTopVisibleSplitAttributes()
}
- @Suppress("DEPRECATION")
- @RequiresWindowSdkExtension(3)
- override fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
- WindowSdkExtensions.getInstance().requireExtensionVersion(3)
-
- embeddingExtension.updateSplitAttributes(
- splitInfo.token,
- adapter.translateSplitAttributes(splitAttributes)
- )
+ private fun setDefaultSplitAttributeCalculatorIfNeeded() {
+ // Setting a default SplitAttributeCalculator if the EmbeddingConfiguration is set,
+ // in order to ensure the dimAreaBehavior in the SplitAttribute is up-to-date.
+ if (
+ windowSdkExtensions.extensionVersion >= 5 &&
+ !isCustomSplitAttributeCalculatorSet &&
+ adapter.embeddingConfiguration != null
+ ) {
+ embeddingExtension.setSplitAttributesCalculator { params ->
+ adapter.translateSplitAttributes(adapter.translate(params.defaultSplitAttributes))
+ }
+ }
}
- @Suppress("DEPRECATION")
@RequiresWindowSdkExtension(3)
- override fun setLaunchingActivityStack(
- options: ActivityOptions,
- token: IBinder
- ): ActivityOptions {
- WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+ override fun invalidateVisibleActivityStacks() {
+ windowSdkExtensions.requireExtensionVersion(3)
- return embeddingExtension.setLaunchingActivityStack(options, token)
+ embeddingExtension.invalidateVisibleActivityStacks()
+ }
+
+ /**
+ * Updates top [activityStacks][ActivityStack] layouts, which will trigger [SplitAttributes]
+ * calculator and [ActivityStackAttributes] calculator if set.
+ */
+ private fun ActivityEmbeddingComponent.invalidateVisibleActivityStacks() {
+ // Note that this API also updates overlay container regardless of its naming.
+ invalidateTopVisibleSplitAttributes()
+ }
+
+ @Suppress("Deprecation") // To compat with device with extension version 3 and 4.
+ @RequiresWindowSdkExtension(3)
+ override fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
+ windowSdkExtensions.requireExtensionVersion(3)
+
+ if (windowSdkExtensions.extensionVersion >= 5) {
+ embeddingExtension.updateSplitAttributes(
+ splitInfo.getToken(),
+ adapter.translateSplitAttributes(splitAttributes)
+ )
+ } else {
+ embeddingExtension.updateSplitAttributes(
+ splitInfo.getBinder(),
+ adapter.translateSplitAttributes(splitAttributes)
+ )
+ }
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle {
+ windowSdkExtensions.requireExtensionVersion(5)
+
+ ActivityEmbeddingOptionsImpl.setActivityStackToken(options, activityStack.getToken())
+ return options
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun setOverlayCreateParams(
+ options: Bundle,
+ overlayCreateParams: OverlayCreateParams
+ ): Bundle =
+ options.apply {
+ ActivityEmbeddingOptionsImpl.setOverlayCreateParams(options, overlayCreateParams)
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ ) {
+ windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ overlayController!!.overlayAttributesCalculator = calculator
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun clearOverlayAttributesCalculator() {
+ windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ overlayController!!.overlayAttributesCalculator = null
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+ windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ overlayController!!.updateOverlayAttributes(overlayTag, overlayAttributes)
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: JetpackConsumer<OverlayInfo>,
+ ) {
+ overlayController?.addOverlayInfoCallback(
+ overlayTag,
+ executor,
+ overlayInfoCallback,
+ )
+ ?: apply {
+ Log.w(TAG, "overlayInfo is not supported on device less than version 5")
+
+ overlayInfoCallback.accept(
+ OverlayInfo(
+ overlayTag,
+ currentOverlayAttributes = null,
+ activityStack = null,
+ )
+ )
+ }
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer<OverlayInfo>) {
+ overlayController?.removeOverlayInfoCallback(overlayInfoCallback)
+ }
+
+ @RequiresWindowSdkExtension(6)
+ override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+ activity: Activity,
+ callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+ ) {
+ activityWindowInfoCallbackController?.addCallback(activity, callback)
+ ?: apply {
+ Log.w(
+ TAG,
+ "EmbeddedActivityWindowInfo is not supported on device less than version 6"
+ )
+ }
+ }
+
+ @RequiresWindowSdkExtension(6)
+ override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+ callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+ ) {
+ activityWindowInfoCallbackController?.removeCallback(callback)
}
companion object {
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
new file mode 100644
index 0000000..930acb2
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.RequiresWindowSdkExtension
+
+/**
+ * Configurations of Activity Embedding environment that defines how the embedded Activities behave.
+ *
+ * @constructor The [EmbeddingConfiguration] constructor. The properties are undefined if not
+ * specified.
+ * @property dimAreaBehavior The requested dim area behavior.
+ * @see ActivityEmbeddingController.setEmbeddingConfiguration
+ */
+class EmbeddingConfiguration
+@JvmOverloads
+constructor(
+ @RequiresWindowSdkExtension(5) val dimAreaBehavior: DimAreaBehavior = DimAreaBehavior.UNDEFINED
+) {
+ /**
+ * The area of dimming to apply.
+ *
+ * @see [android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND]
+ */
+ class DimAreaBehavior private constructor(@IntRange(from = 0, to = 2) internal val value: Int) {
+ companion object {
+ /**
+ * The dim area is not defined.
+ *
+ * This is the default value while building a [EmbeddingConfiguration]. This would also
+ * keep the existing dim area configuration of the current Activity Embedding
+ * environment unchanged when [ActivityEmbeddingController.setEmbeddingConfiguration] is
+ * called.
+ *
+ * @see ActivityEmbeddingController.setEmbeddingConfiguration
+ */
+ @JvmField val UNDEFINED = DimAreaBehavior(0)
+
+ /**
+ * The dim effect is applying on the [ActivityStack] of the Activity window when needed.
+ * If the [ActivityStack] is split and displayed side-by-side with another
+ * [ActivityStack], the dim effect is applying only on the [ActivityStack] of the
+ * requested Activity.
+ */
+ @JvmField val ON_ACTIVITY_STACK = DimAreaBehavior(1)
+
+ /**
+ * The dimming effect is applying on the area of the whole Task when needed. If the
+ * embedded transparent activity is split and displayed side-by-side with another
+ * activity, the dim effect is applying on the Task, which across over the two
+ * [ActivityStack]s.
+ *
+ * This is the default dim area configuration of the Activity Embedding environment,
+ * before the [DimAreaBehavior] is explicitly set by
+ * [ActivityEmbeddingController.setEmbeddingConfiguration].
+ */
+ @JvmField val ON_TASK = DimAreaBehavior(2)
+ }
+
+ override fun toString(): String {
+ return "DimAreaBehavior=" +
+ when (value) {
+ 0 -> "UNDEFINED"
+ 1 -> "ON_ACTIVITY_STACK"
+ 2 -> "ON_TASK"
+ else -> "UNKNOWN"
+ }
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EmbeddingConfiguration) return false
+
+ if (dimAreaBehavior != other.dimAreaBehavior) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return dimAreaBehavior.hashCode()
+ }
+
+ override fun toString(): String = "EmbeddingConfiguration{$dimAreaBehavior}"
+
+ /** Builder for creating an instance of [EmbeddingConfiguration]. */
+ class Builder {
+ private var mDimAreaBehavior = DimAreaBehavior.UNDEFINED
+
+ /**
+ * Sets the dim area behavior. By default, the [DimAreaBehavior.UNDEFINED] is used if not
+ * set.
+ *
+ * @param area The dim area.
+ * @return This [Builder]
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ fun setDimAreaBehavior(area: DimAreaBehavior): Builder = apply { mDimAreaBehavior = area }
+
+ /**
+ * Builds a[EmbeddingConfiguration] instance.
+ *
+ * @return The new [EmbeddingConfiguration] instance.
+ */
+ fun build(): EmbeddingConfiguration = EmbeddingConfiguration(mDimAreaBehavior)
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index 8c63f2a..39ac592 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -17,10 +17,12 @@
package androidx.window.embedding
import android.app.Activity
-import android.app.ActivityOptions
-import android.os.IBinder
+import android.os.Bundle
+import androidx.core.util.Consumer
import androidx.window.RequiresWindowSdkExtension
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import java.util.concurrent.Executor
/**
* Adapter interface for different historical versions of activity embedding OEM interface in
@@ -34,10 +36,17 @@
interface EmbeddingCallbackInterface {
fun onSplitInfoChanged(splitInfo: List<SplitInfo>)
+
+ fun onActivityStackChanged(activityStacks: List<ActivityStack>)
}
fun isActivityEmbedded(activity: Activity): Boolean
+ @RequiresWindowSdkExtension(5)
+ fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
+
+ @RequiresWindowSdkExtension(5) fun unpinTopActivityStack(taskId: Int)
+
@RequiresWindowSdkExtension(2)
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -45,11 +54,50 @@
@RequiresWindowSdkExtension(2) fun clearSplitAttributesCalculator()
- @RequiresWindowSdkExtension(3)
- fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+ @RequiresWindowSdkExtension(5)
+ fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
- @RequiresWindowSdkExtension(3) fun invalidateTopVisibleSplitAttributes()
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
+
+ @RequiresWindowSdkExtension(5) fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+ @RequiresWindowSdkExtension(5)
+ fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
+
+ @RequiresWindowSdkExtension(3) fun invalidateVisibleActivityStacks()
@RequiresWindowSdkExtension(3)
fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ )
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION) fun clearOverlayAttributesCalculator()
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: Consumer<OverlayInfo>,
+ )
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>)
+
+ @RequiresWindowSdkExtension(6)
+ fun addEmbeddedActivityWindowInfoCallbackForActivity(
+ activity: Activity,
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ )
+
+ @RequiresWindowSdkExtension(6)
+ fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ )
}
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index 11e0f41..aaac205 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -17,11 +17,10 @@
package androidx.window.embedding
import android.app.Activity
-import android.app.ActivityOptions
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
-import android.os.IBinder
+import android.os.Bundle
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
@@ -30,13 +29,14 @@
import androidx.core.util.Consumer
import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowProperties
+import androidx.window.WindowSdkExtensions
import androidx.window.core.BuildConfig
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.core.VerificationMode
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import androidx.window.embedding.ExtensionEmbeddingBackend.Api31Impl.isSplitPropertyEnabled
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
@@ -52,11 +52,11 @@
) : EmbeddingBackend {
@VisibleForTesting val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
- private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
+ private val embeddingCallback = EmbeddingCallbackImpl()
init {
splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
- embeddingExtension?.setEmbeddingCallback(splitInfoEmbeddingCallback)
+ embeddingExtension?.setEmbeddingCallback(embeddingCallback)
}
companion object {
@@ -87,18 +87,30 @@
applicationContext: Context
): EmbeddingInterfaceCompat? {
var impl: EmbeddingInterfaceCompat? = null
+ val version = WindowSdkExtensions.getInstance().extensionVersion
try {
if (
- isExtensionVersionSupported(ExtensionsUtil.safeVendorApiLevel) &&
- EmbeddingCompat.isEmbeddingAvailable()
+ isExtensionVersionSupported(version) && EmbeddingCompat.isEmbeddingAvailable()
) {
impl =
EmbeddingBackend::class.java.classLoader?.let { loader ->
+ val embeddingExtension = EmbeddingCompat.embeddingComponent()
+ val adapter = EmbeddingAdapter(PredicateAdapter(loader))
EmbeddingCompat(
- EmbeddingCompat.embeddingComponent(),
- EmbeddingAdapter(PredicateAdapter(loader)),
+ embeddingExtension,
+ adapter,
ConsumerAdapter(loader),
- applicationContext
+ applicationContext,
+ if (version >= OVERLAY_FEATURE_VERSION) {
+ OverlayControllerImpl(embeddingExtension, adapter)
+ } else {
+ null
+ },
+ if (version >= 6) {
+ ActivityWindowInfoCallbackController(embeddingExtension)
+ } else {
+ null
+ },
)
}
// TODO(b/190433400): Check API conformance
@@ -278,11 +290,7 @@
val callbackWrapper = SplitListenerWrapper(activity, executor, callback)
splitChangeCallbacks.add(callbackWrapper)
- if (splitInfoEmbeddingCallback.lastInfo != null) {
- callbackWrapper.accept(splitInfoEmbeddingCallback.lastInfo!!)
- } else {
- callbackWrapper.accept(emptyList())
- }
+ callbackWrapper.accept(embeddingCallback.lastInfo)
}
}
@@ -298,11 +306,13 @@
}
/**
- * Extension callback implementation of the split information. Keeps track of last reported
+ * Extension callback implementation of the embedding information. Keeps track of last reported
* values.
*/
internal inner class EmbeddingCallbackImpl : EmbeddingCallbackInterface {
- var lastInfo: List<SplitInfo>? = null
+ var lastInfo: List<SplitInfo> = emptyList()
+
+ var lastActivityStacks: List<ActivityStack> = emptyList()
override fun onSplitInfoChanged(splitInfo: List<SplitInfo>) {
lastInfo = splitInfo
@@ -310,6 +320,10 @@
callbackWrapper.accept(splitInfo)
}
}
+
+ override fun onActivityStackChanged(activityStacks: List<ActivityStack>) {
+ lastActivityStacks = activityStacks
+ }
}
private fun areExtensionsAvailable(): Boolean {
@@ -336,6 +350,16 @@
return embeddingExtension?.isActivityEmbedded(activity) ?: false
}
+ @RequiresWindowSdkExtension(5)
+ override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+ return embeddingExtension?.pinTopActivityStack(taskId, splitPinRule) ?: false
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun unpinTopActivityStack(taskId: Int) {
+ embeddingExtension?.unpinTopActivityStack(taskId)
+ }
+
@RequiresWindowSdkExtension(2)
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -348,33 +372,51 @@
globalLock.withLock { embeddingExtension?.clearSplitAttributesCalculator() }
}
- override fun getActivityStack(activity: Activity): ActivityStack? {
+ override fun getActivityStack(activity: Activity): ActivityStack? =
globalLock.withLock {
- val lastInfo: List<SplitInfo> = splitInfoEmbeddingCallback.lastInfo ?: return null
- for (info in lastInfo) {
- if (activity !in info) {
- continue
- }
- if (activity in info.primaryActivityStack) {
- return info.primaryActivityStack
- }
- if (activity in info.secondaryActivityStack) {
- return info.secondaryActivityStack
- }
- }
- return null
+ embeddingCallback.lastActivityStacks.find { activityStack -> activity in activityStack }
+ ?: getActivityStackFromSplitInfoList(activity)
}
+
+ @GuardedBy("globalLock")
+ private fun getActivityStackFromSplitInfoList(activity: Activity): ActivityStack? {
+ for (info in embeddingCallback.lastInfo) {
+ if (activity !in info) {
+ continue
+ }
+ if (activity in info.primaryActivityStack) {
+ return info.primaryActivityStack
+ }
+ if (activity in info.secondaryActivityStack) {
+ return info.secondaryActivityStack
+ }
+ }
+ return null
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle =
+ embeddingExtension?.setLaunchingActivityStack(options, activityStack) ?: options
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun setOverlayCreateParams(
+ options: Bundle,
+ overlayCreateParams: OverlayCreateParams,
+ ): Bundle = embeddingExtension?.setOverlayCreateParams(options, overlayCreateParams) ?: options
+
+ @RequiresWindowSdkExtension(5)
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ embeddingExtension?.finishActivityStacks(activityStacks)
+ }
+
+ @RequiresWindowSdkExtension(5)
+ override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+ embeddingExtension?.setEmbeddingConfiguration(embeddingConfig)
}
@RequiresWindowSdkExtension(3)
- override fun setLaunchingActivityStack(
- options: ActivityOptions,
- token: IBinder
- ): ActivityOptions = embeddingExtension?.setLaunchingActivityStack(options, token) ?: options
-
- @RequiresWindowSdkExtension(3)
- override fun invalidateTopVisibleSplitAttributes() {
- embeddingExtension?.invalidateTopVisibleSplitAttributes()
+ override fun invalidateVisibleActivityStacks() {
+ embeddingExtension?.invalidateVisibleActivityStacks()
}
@RequiresWindowSdkExtension(3)
@@ -382,6 +424,60 @@
embeddingExtension?.updateSplitAttributes(splitInfo, splitAttributes)
}
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ ) {
+ embeddingExtension?.setOverlayAttributesCalculator(calculator)
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun clearOverlayAttributesCalculator() {
+ embeddingExtension?.clearOverlayAttributesCalculator()
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+ embeddingExtension?.updateOverlayAttributes(overlayTag, overlayAttributes)
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: Consumer<OverlayInfo>,
+ ) {
+ embeddingExtension?.addOverlayInfoCallback(overlayTag, executor, overlayInfoCallback)
+ // Send an empty OverlayInfo if the extension does not exist.
+ ?: overlayInfoCallback.accept(
+ OverlayInfo(
+ overlayTag,
+ currentOverlayAttributes = null,
+ activityStack = null,
+ )
+ )
+ }
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>) {
+ embeddingExtension?.removeOverlayInfoCallback(overlayInfoCallback)
+ }
+
+ @RequiresWindowSdkExtension(6)
+ override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+ activity: Activity,
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ ) {
+ embeddingExtension?.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
+ }
+
+ @RequiresWindowSdkExtension(6)
+ override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+ callback: Consumer<EmbeddedActivityWindowInfo>
+ ) {
+ embeddingExtension?.removeEmbeddedActivityWindowInfoCallbackForActivity(callback)
+ }
+
@RequiresApi(31)
private object Api31Impl {
fun isSplitPropertyEnabled(context: Context): SplitController.SplitSupportStatus {
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
new file mode 100644
index 0000000..6c446102
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 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.embedding
+
+import androidx.annotation.RestrictTo
+
+/**
+ * The attributes to describe how an overlay container should look like.
+ *
+ * @constructor creates an overlay attributes.
+ * @property bounds The overlay container's [EmbeddingBounds], which defaults to
+ * [EmbeddingBounds.BOUNDS_EXPANDED] if not specified.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayAttributes
+@JvmOverloads
+constructor(val bounds: EmbeddingBounds = EmbeddingBounds.BOUNDS_EXPANDED) {
+
+ override fun toString(): String =
+ "${OverlayAttributes::class.java.simpleName}: {" + "bounds=$bounds" + "}"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is OverlayAttributes) return false
+ return bounds == other.bounds
+ }
+
+ override fun hashCode(): Int = bounds.hashCode()
+
+ /** The [OverlayAttributes] builder. */
+ class Builder {
+
+ private var bounds = EmbeddingBounds.BOUNDS_EXPANDED
+
+ /**
+ * Sets the overlay bounds, which defaults to [EmbeddingBounds.BOUNDS_EXPANDED] if not
+ * specified.
+ *
+ * @param bounds The [EmbeddingBounds] of the overlay [ActivityStack].
+ * @return The [OverlayAttributes] builder.
+ */
+ fun setBounds(bounds: EmbeddingBounds): Builder = apply { this.bounds = bounds }
+
+ /** Builds [OverlayAttributes]. */
+ fun build(): OverlayAttributes = OverlayAttributes(bounds)
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
new file mode 100644
index 0000000..d357bf9
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.content.res.Configuration
+import androidx.annotation.RestrictTo
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * The parameter container used to report the current device and window state in
+ * [OverlayController.setOverlayAttributesCalculator] and references the corresponding overlay
+ * [ActivityStack] by [overlayTag].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayAttributesCalculatorParams
+internal constructor(
+ /** The parent container's [WindowMetrics] */
+ val parentWindowMetrics: WindowMetrics,
+ /** The parent container's [Configuration] */
+ val parentConfiguration: Configuration,
+ /** The parent container's [WindowLayoutInfo] */
+ val parentWindowLayoutInfo: WindowLayoutInfo,
+ /**
+ * The unique identifier of the overlay [ActivityStack] specified by [OverlayCreateParams.tag]
+ */
+ val overlayTag: String,
+ /**
+ * The overlay [ActivityStack]'s [OverlayAttributes] specified by [overlayTag], which is the
+ * [OverlayAttributes] that is not calculated by calculator. It should be either initialized by
+ * [OverlayCreateParams.overlayAttributes] or [OverlayController.updateOverlayAttributes].
+ */
+ val defaultOverlayAttributes: OverlayAttributes,
+) {
+ override fun toString(): String =
+ "${OverlayAttributesCalculatorParams::class.java}:{" +
+ "parentWindowMetrics=$parentWindowMetrics" +
+ "parentConfiguration=$parentConfiguration" +
+ "parentWindowLayoutInfo=$parentWindowLayoutInfo" +
+ "overlayTag=$overlayTag" +
+ "defaultOverlayAttributes=$defaultOverlayAttributes"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayController.kt b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
new file mode 100644
index 0000000..e2b347c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+/**
+ * The controller to manage overlay [ActivityStack], which is launched by the activityOptions that
+ * [setOverlayCreateParams].
+ *
+ * See linked sample below for how to launch an [android.app.Activity] into an overlay
+ * [ActivityStack].
+ *
+ * Supported operations are:
+ * - [setOverlayAttributesCalculator] to update overlay presentation with device or window state and
+ * [OverlayCreateParams.tag].
+ *
+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayController
+@VisibleForTesting
+internal constructor(private val backend: EmbeddingBackend) {
+
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ internal fun setOverlayCreateParams(
+ options: Bundle,
+ overlayCreateParams: OverlayCreateParams,
+ ): Bundle = backend.setOverlayCreateParams(options, overlayCreateParams)
+
+ /**
+ * Sets an overlay calculator function to update overlay presentation with device or window
+ * state and [OverlayCreateParams.tag].
+ *
+ * Overlay calculator function is triggered with following scenarios:
+ * - An overlay [ActivityStack] is launched.
+ * - The parent task configuration changes. i.e. orientation change, enter/exit multi-window
+ * mode or resize apps in multi-window mode.
+ * - Device folding state changes.
+ * - Device is attached to an external display and the app is forwarded to that display.
+ *
+ * If there's no [calculator] set, the overlay presentation will be calculated with the previous
+ * set [OverlayAttributes], either from [OverlayCreateParams] to initialize the overlay
+ * container, or from the runtime API to update the overlay container's [OverlayAttributes].
+ *
+ * See the sample linked below for how to use [OverlayAttributes] calculator
+ *
+ * @param calculator The overlay calculator function to compute [OverlayAttributes] by
+ * [OverlayAttributesCalculatorParams]. It will replace the previously set if it exists.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 6.
+ * @sample androidx.window.samples.embedding.overlayAttributesCalculatorSample
+ */
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun setOverlayAttributesCalculator(
+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+ ) {
+ backend.setOverlayAttributesCalculator(calculator)
+ }
+
+ /**
+ * Clears the overlay calculator function previously set by [setOverlayAttributesCalculator].
+ *
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 6.
+ */
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun clearOverlayAttributesCalculator() {
+ backend.clearOverlayAttributesCalculator()
+ }
+
+ /**
+ * Updates [OverlayAttributes] of the overlay [ActivityStack] specified by [overlayTag]. It's no
+ * op if there's no such overlay [ActivityStack] associated with [overlayTag].
+ *
+ * If an [OverlayAttributes] calculator function is specified, the updated [overlayAttributes]
+ * will be passed by [OverlayAttributesCalculatorParams.defaultOverlayAttributes] when the
+ * calculator function applies to the overlay [ActivityStack] specified by [overlayTag].
+ *
+ * In most cases it is suggested to use
+ * [ActivityEmbeddingController.invalidateVisibleActivityStacks] if a calculator has been set
+ * through [OverlayController.setOverlayAttributesCalculator].
+ *
+ * @param overlayTag The overlay [ActivityStack]'s tag
+ * @param overlayAttributes The [OverlayAttributes] to update
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 6.
+ */
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+ backend.updateOverlayAttributes(overlayTag, overlayAttributes)
+ }
+
+ /**
+ * A [Flow] of [OverlayInfo] that [overlayTag] is associated with.
+ *
+ * If there's an active overlay [ActivityStack] associated with [overlayTag], it will be
+ * reported in [OverlayInfo.activityStack]. Otherwise, [OverlayInfo.activityStack] is `null`.
+ *
+ * Note that launching an overlay [ActivityStack] only supports on the device with
+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+ * [WindowSdkExtensions.extensionVersion] is less than 6, this flow will always report
+ * [OverlayInfo] without associated [OverlayInfo.activityStack].
+ *
+ * @param overlayTag The overlay [ActivityStack]'s tag which is set through
+ * [OverlayCreateParams]
+ * @return a [Flow] of [OverlayInfo] this [overlayTag] is associated with
+ */
+ @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+ fun overlayInfo(overlayTag: String): Flow<OverlayInfo> = callbackFlow {
+ val listener = Consumer { info: OverlayInfo -> trySend(info) }
+ backend.addOverlayInfoCallback(overlayTag, Runnable::run, listener)
+ awaitClose { backend.removeOverlayInfoCallback(listener) }
+ }
+
+ companion object {
+
+ internal const val OVERLAY_FEATURE_VERSION = 8
+
+ /**
+ * Obtains an instance of [OverlayController].
+ *
+ * @param context the [Context] to initialize the controller with
+ */
+ @JvmStatic
+ fun getInstance(context: Context): OverlayController {
+ val backend = EmbeddingBackend.getInstance(context)
+ return OverlayController(backend)
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
new file mode 100644
index 0000000..885c283d
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.content.res.Configuration
+import android.util.ArrayMap
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityEmbeddingOptionsImpl.getOverlayAttributes
+import androidx.window.embedding.ActivityEmbeddingOptionsImpl.putActivityStackAlignment
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
+import androidx.window.extensions.embedding.ParentContainerInfo
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
+import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
+import androidx.window.layout.util.DensityCompatHelper
+import androidx.window.reflection.Consumer2
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * The core implementation of [OverlayController] APIs, which is implemented by [ActivityStack]
+ * operations in WM Extensions.
+ */
+@Suppress("NewApi") // Suppress #translateWindowMetrics, which requires R.
+@RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+internal open class OverlayControllerImpl(
+ private val embeddingExtension: ActivityEmbeddingComponent,
+ private val adapter: EmbeddingAdapter,
+) {
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock")
+ internal var overlayAttributesCalculator:
+ ((OverlayAttributesCalculatorParams) -> OverlayAttributes)? =
+ null
+ get() = globalLock.withLock { field }
+ set(value) {
+ globalLock.withLock { field = value }
+ }
+
+ /**
+ * Mapping between the overlay container tag and its default [OverlayAttributes]. It's to record
+ * the [OverlayAttributes] updated through [updateOverlayAttributes] and report in
+ * [OverlayAttributesCalculatorParams].
+ */
+ @GuardedBy("globalLock")
+ private val overlayTagToDefaultAttributesMap: MutableMap<String, OverlayAttributes> = ArrayMap()
+
+ /**
+ * Mapping between the overlay container tag and its current [OverlayAttributes] to provide the
+ * [OverlayInfo] updates.
+ */
+ @GuardedBy("globalLock")
+ private val overlayTagToCurrentAttributesMap = ArrayMap<String, OverlayAttributes>()
+
+ @GuardedBy("globalLock")
+ private val overlayTagToContainerMap = ArrayMap<String, ActivityStack>()
+
+ /** The mapping from [OverlayInfo] callback to [activityStacks][ActivityStack] callback. */
+ @GuardedBy("globalLock")
+ private val overlayInfoToActivityStackCallbackMap =
+ ArrayMap<JetpackConsumer<OverlayInfo>, Consumer<List<ActivityStack>>>()
+
+ init {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ embeddingExtension.setActivityStackAttributesCalculator { params ->
+ globalLock.withLock {
+ val parentContainerInfo = params.parentContainerInfo
+ val density =
+ DensityCompatHelper.getInstance()
+ .density(
+ parentContainerInfo.configuration,
+ parentContainerInfo.windowMetrics
+ )
+ val windowMetrics =
+ WindowMetricsCalculator.translateWindowMetrics(
+ parentContainerInfo.windowMetrics,
+ density
+ )
+ val overlayAttributes =
+ calculateOverlayAttributes(
+ params.activityStackTag,
+ params.launchOptions.getOverlayAttributes(),
+ WindowMetricsCalculator.translateWindowMetrics(
+ params.parentContainerInfo.windowMetrics,
+ density
+ ),
+ params.parentContainerInfo.configuration,
+ ExtensionsWindowLayoutInfoAdapter.translate(
+ windowMetrics,
+ parentContainerInfo.windowLayoutInfo
+ ),
+ )
+
+ // TODO(b/295805497): Migrate to either custom animation APIs or new
+ // ActivityStackAttributes APIs.
+ // Set alignment to the bundle options as the hint of the animation direction.
+ params.launchOptions.putActivityStackAlignment(overlayAttributes.bounds)
+ return@setActivityStackAttributesCalculator overlayAttributes
+ .toActivityStackAttributes(parentContainerInfo)
+ }
+ }
+
+ embeddingExtension.registerActivityStackCallback(Runnable::run) { activityStacks ->
+ globalLock.withLock {
+ val lastOverlayTags = overlayTagToContainerMap.keys
+
+ overlayTagToContainerMap.clear()
+ overlayTagToContainerMap.putAll(
+ activityStacks.getOverlayContainers().map { overlayContainer ->
+ Pair(overlayContainer.tag!!, overlayContainer)
+ }
+ )
+
+ cleanUpDismissedOverlayContainerRecords(lastOverlayTags)
+ }
+ }
+ }
+
+ /**
+ * Clean up records associated with dismissed overlay [activityStacks][ActivityStack] when
+ * there's a [ActivityStack] state update.
+ *
+ * The dismissed overlay [activityStacks][ActivityStack] are identified by comparing the
+ * differences of [ActivityStack] state before and after update.
+ *
+ * @param lastOverlayTags Overlay containers' tag before applying [ActivityStack] state update.
+ */
+ @GuardedBy("globalLock")
+ private fun cleanUpDismissedOverlayContainerRecords(lastOverlayTags: Set<String>) {
+ if (lastOverlayTags.isEmpty()) {
+ // If there's no last overlay container, return.
+ return
+ }
+
+ val dismissedOverlayTags = ArrayList<String>()
+ val currentOverlayTags = overlayTagToContainerMap.keys
+
+ for (overlayTag in lastOverlayTags) {
+ if (
+ overlayTag !in currentOverlayTags &&
+ // If an overlay activityStack is not in the current overlay container list,
+ // check
+ // whether the activityStack does really not exist in WM Extensions in case
+ // an overlay container is just launched, but th WM Jetpack hasn't received the
+ // update yet.
+ embeddingExtension.getActivityStackToken(overlayTag) == null
+ ) {
+ dismissedOverlayTags.add(overlayTag)
+ }
+ }
+
+ for (overlayTag in dismissedOverlayTags) {
+ overlayTagToDefaultAttributesMap.remove(overlayTag)
+ overlayTagToCurrentAttributesMap.remove(overlayTag)
+ }
+ }
+
+ /**
+ * Calculates the [OverlayAttributes] to report to the [ActivityStackAttributes] calculator.
+ *
+ * The calculator then computes [ActivityStackAttributes] for rendering the overlay
+ * [ActivityStack].
+ *
+ * @param tag The overlay [ActivityStack].
+ * @param initialOverlayAttrs The [OverlayCreateParams.overlayAttributes] that used to launching
+ * this overlay [ActivityStack]
+ * @param windowMetrics The parent window container's [WindowMetrics]
+ * @param configuration The parent window container's [Configuration]
+ * @param windowLayoutInfo The parent window container's [WindowLayoutInfo]
+ */
+ @VisibleForTesting
+ internal fun calculateOverlayAttributes(
+ tag: String,
+ initialOverlayAttrs: OverlayAttributes?,
+ windowMetrics: WindowMetrics,
+ configuration: Configuration,
+ windowLayoutInfo: WindowLayoutInfo,
+ ): OverlayAttributes {
+ val defaultOverlayAttrs =
+ getUpdatedOverlayAttributes(tag)
+ ?: initialOverlayAttrs
+ ?: throw IllegalArgumentException(
+ "Can't retrieve overlay attributes from launch options"
+ )
+ val currentOverlayAttrs =
+ overlayAttributesCalculator?.invoke(
+ OverlayAttributesCalculatorParams(
+ windowMetrics,
+ configuration,
+ windowLayoutInfo,
+ tag,
+ defaultOverlayAttrs,
+ )
+ ) ?: defaultOverlayAttrs
+
+ overlayTagToCurrentAttributesMap[tag] = currentOverlayAttrs
+
+ return currentOverlayAttrs
+ }
+
+ @VisibleForTesting
+ internal open fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =
+ overlayTagToDefaultAttributesMap[overlayTag]
+
+ internal open fun updateOverlayAttributes(
+ overlayTag: String,
+ overlayAttributes: OverlayAttributes
+ ) {
+ globalLock.withLock {
+ val activityStackToken =
+ overlayTagToContainerMap[overlayTag]?.activityStackToken
+ // Users may call this API before any callback coming. Try to ask platform if
+ // this container exists.
+ ?: embeddingExtension.getActivityStackToken(overlayTag)
+ // Early return if there's no such ActivityStack associated with the tag.
+ ?: return
+
+ embeddingExtension.updateActivityStackAttributes(
+ activityStackToken,
+ overlayAttributes.toActivityStackAttributes(
+ embeddingExtension.getParentContainerInfo(activityStackToken)!!
+ )
+ )
+
+ // Update the tag-overlayAttributes map, which will be treated as the default
+ // overlayAttributes in calculator.
+ overlayTagToDefaultAttributesMap[overlayTag] = overlayAttributes
+ overlayTagToCurrentAttributesMap[overlayTag] = overlayAttributes
+ }
+ }
+
+ private fun OverlayAttributes.toActivityStackAttributes(
+ parentContainerInfo: ParentContainerInfo
+ ): ActivityStackAttributes =
+ ActivityStackAttributes.Builder()
+ .setRelativeBounds(
+ EmbeddingBounds.translateEmbeddingBounds(
+ bounds,
+ adapter.translate(parentContainerInfo)
+ )
+ .toRect()
+ )
+ .setWindowAttributes(adapter.translateWindowAttributes())
+ .build()
+
+ private fun List<ActivityStack>.getOverlayContainers(): List<ActivityStack> =
+ filter { activityStack -> activityStack.tag != null }.toList()
+
+ open fun addOverlayInfoCallback(
+ overlayTag: String,
+ executor: Executor,
+ overlayInfoCallback: JetpackConsumer<OverlayInfo>,
+ ) {
+ globalLock.withLock {
+ val callback =
+ Consumer2<List<ActivityStack>> { activityStacks ->
+ val overlayInfoList =
+ activityStacks.filter { activityStack -> activityStack.tag == overlayTag }
+ if (overlayInfoList.size > 1) {
+ throw IllegalStateException(
+ "There must be at most one overlay ActivityStack with $overlayTag"
+ )
+ }
+ val overlayInfo =
+ if (overlayInfoList.isEmpty()) {
+ OverlayInfo(
+ overlayTag,
+ currentOverlayAttributes = null,
+ activityStack = null,
+ )
+ } else {
+ overlayInfoList.first().toOverlayInfo()
+ }
+ overlayInfoCallback.accept(overlayInfo)
+ }
+ overlayInfoToActivityStackCallbackMap[overlayInfoCallback] = callback
+
+ embeddingExtension.registerActivityStackCallback(executor, callback)
+ }
+ }
+
+ private fun ActivityStack.toOverlayInfo(): OverlayInfo =
+ OverlayInfo(
+ tag!!,
+ overlayTagToCurrentAttributesMap[tag!!],
+ adapter.translate(this),
+ )
+
+ open fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer<OverlayInfo>) {
+ globalLock.withLock {
+ val callback = overlayInfoToActivityStackCallbackMap.remove(overlayInfoCallback)
+ if (callback != null) {
+ embeddingExtension.unregisterActivityStackCallback(callback)
+ }
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
new file mode 100644
index 0000000..25548c0
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023 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.embedding
+
+import androidx.annotation.RestrictTo
+import java.util.UUID
+
+/**
+ * The parameter container to create an overlay [ActivityStack].
+ *
+ * If there's an shown overlay [ActivityStack] associated with the [tag], the existing
+ * [ActivityStack] will be:
+ * - Dismissed if the overlay [ActivityStack] is in different task from the launched one
+ * - Updated with [OverlayAttributes] if the overlay [ActivityStack] is in the same task.
+ *
+ * See [android.os.Bundle.setOverlayCreateParams] for how to create an overlay [ActivityStack].
+ *
+ * @constructor creates a parameter container to launch an overlay [ActivityStack].
+ * @property tag The unique identifier of the overlay [ActivityStack], which will be generated
+ * automatically if not specified.
+ * @property overlayAttributes The attributes of the overlay [ActivityStack], which defaults to the
+ * default value of [OverlayAttributes].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayCreateParams
+@JvmOverloads
+constructor(
+ val tag: String = generateOverlayTag(),
+ val overlayAttributes: OverlayAttributes = OverlayAttributes.Builder().build(),
+) {
+ override fun toString(): String =
+ "${OverlayCreateParams::class.simpleName}:{ " +
+ ", tag=$tag" +
+ ", attrs=$overlayAttributes" +
+ "}"
+
+ /** The [OverlayCreateParams] builder. */
+ class Builder {
+ private var tag: String? = null
+ private var launchAttrs: OverlayAttributes? = null
+
+ /**
+ * Sets the overlay [ActivityStack]'s unique identifier. The builder will generate one
+ * automatically if not specified.
+ *
+ * @param tag The unique identifier of the overlay [ActivityStack] to create.
+ * @return The [OverlayCreateParams] builder.
+ */
+ fun setTag(tag: String): Builder = apply { this.tag = tag }
+
+ /**
+ * Sets the overlay [ActivityStack]'s attributes, which defaults to the default value of
+ * [OverlayAttributes.Builder].
+ *
+ * @param attrs The [OverlayAttributes].
+ * @return The [OverlayCreateParams] builder.
+ */
+ fun setOverlayAttributes(attrs: OverlayAttributes): Builder = apply { launchAttrs = attrs }
+
+ /** Builds the [OverlayCreateParams] */
+ fun build(): OverlayCreateParams =
+ OverlayCreateParams(
+ tag ?: generateOverlayTag(),
+ launchAttrs ?: OverlayAttributes.Builder().build()
+ )
+ }
+
+ companion object {
+
+ /** A helper function to generate a random unique identifier. */
+ @JvmStatic
+ fun generateOverlayTag(): String = UUID.randomUUID().toString().substring(IntRange(0, 32))
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
new file mode 100644
index 0000000..6f76742
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.embedding
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+
+/** Describes an overlay [ActivityStack] associated with [OverlayCreateParams.tag]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayInfo
+internal constructor(
+ /** The unique identifier associated with the overlay [ActivityStack]. */
+ val overlayTag: String,
+ /**
+ * The [OverlayAttributes] of the overlay [ActivityStack] if it exists, or `null` if there's no
+ * such a [ActivityStack]
+ */
+ val currentOverlayAttributes: OverlayAttributes?,
+ /**
+ * The overlay [ActivityStack] associated with [overlayTag], or `null` if there's no such a
+ * [ActivityStack].
+ */
+ val activityStack: ActivityStack?
+) {
+ operator fun contains(activity: Activity): Boolean = activityStack?.contains(activity) ?: false
+
+ override fun toString(): String =
+ "${OverlayInfo::class.java.simpleName}: {" +
+ "tag=$overlayTag" +
+ ", currentOverlayAttrs=$currentOverlayAttributes" +
+ ", activityStack=$activityStack" +
+ "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
new file mode 100644
index 0000000..9edb443
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.content.res.Configuration
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.core.Bounds
+import androidx.window.layout.WindowLayoutInfo
+
+/**
+ * The parent container information directly passed from WM Extensions, which is created to make
+ * test implementation easier.
+ *
+ * @property windowBounds The parent container's [Bounds].
+ * @property windowLayoutInfo The parent container's [WindowLayoutInfo].
+ * @property windowInsets The parent container's [WindowInsetsCompat].
+ * @property configuration The parent container's [Configuration].
+ * @property density The parent container's density in DP, which has the same unit as
+ * [android.util.DisplayMetrics.density].
+ */
+internal data class ParentContainerInfo(
+ /** The parent container's [Bounds]. */
+ val windowBounds: Bounds,
+ /** The parent container's [WindowLayoutInfo]. */
+ val windowLayoutInfo: WindowLayoutInfo,
+ /** The parent container's [WindowInsetsCompat] */
+ val windowInsets: WindowInsetsCompat,
+ /** The parent container's [Configuration]. */
+ val configuration: Configuration,
+ /**
+ * The parent container's density in DP, which has the same unit as
+ * [android.util.DisplayMetrics.density].
+ */
+ val density: Float,
+)
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
index 55905cc..4484493 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -23,6 +23,7 @@
import android.content.res.XmlResourceParser
import androidx.annotation.XmlRes
import androidx.window.R
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion.DEFAULT
import androidx.window.embedding.EmbeddingAspectRatio.Companion.buildAspectRatioFromValue
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
@@ -168,6 +169,37 @@
ALWAYS.value
)
val clearTop = typedArray.getBoolean(R.styleable.SplitPairRule_clearTop, false)
+ val animationBackgroundColor =
+ typedArray.getColor(R.styleable.SplitPairRule_animationBackgroundColor, 0)
+ val openAnimation =
+ typedArray.getInt(R.styleable.SplitPairRule_splitOpenAnimation, DEFAULT.value)
+ val closeAnimation =
+ typedArray.getInt(R.styleable.SplitPairRule_splitCloseAnimation, DEFAULT.value)
+ val changeAnimation =
+ typedArray.getInt(R.styleable.SplitPairRule_splitChangeAnimation, DEFAULT.value)
+ typedArray.recycle()
+
+ val animationParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
+ )
+ .setOpenAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ openAnimation
+ )
+ )
+ .setCloseAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ closeAnimation
+ )
+ )
+ .setChangeAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ changeAnimation
+ )
+ )
+ .build()
val defaultAttrs =
SplitAttributes.Builder()
@@ -175,6 +207,7 @@
.setLayoutDirection(
SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
)
+ .setAnimationParams(animationParams)
.build()
SplitPairRule.Builder(emptySet())
@@ -249,6 +282,46 @@
R.styleable.SplitPlaceholderRule_splitLayoutDirection,
LOCALE.value
)
+ val animationBackgroundColor =
+ typedArray.getColor(R.styleable.SplitPlaceholderRule_animationBackgroundColor, 0)
+ val openAnimation =
+ typedArray.getInt(
+ R.styleable.SplitPlaceholderRule_splitOpenAnimation,
+ DEFAULT.value
+ )
+ val closeAnimation =
+ typedArray.getInt(
+ R.styleable.SplitPlaceholderRule_splitCloseAnimation,
+ DEFAULT.value
+ )
+ val changeAnimation =
+ typedArray.getInt(
+ R.styleable.SplitPlaceholderRule_splitChangeAnimation,
+ DEFAULT.value
+ )
+ typedArray.recycle()
+
+ val animationParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
+ )
+ .setOpenAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ openAnimation
+ )
+ )
+ .setCloseAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ closeAnimation
+ )
+ )
+ .setChangeAnimation(
+ EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+ changeAnimation
+ )
+ )
+ .build()
val defaultAttrs =
SplitAttributes.Builder()
@@ -256,6 +329,7 @@
.setLayoutDirection(
SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
)
+ .setAnimationParams(animationParams)
.build()
val packageName = context.applicationContext.packageName
val placeholderActivityClassName =
diff --git a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
index 400ff6a..8ff5de4 100644
--- a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
@@ -18,25 +18,44 @@
import android.app.Activity
import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.IBinder
+import android.view.WindowMetrics
import androidx.annotation.VisibleForTesting
import androidx.window.SafeWindowExtensionsProvider
+import androidx.window.WindowSdkExtensions
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.WindowExtensions
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.extensions.core.util.function.Function
+import androidx.window.extensions.core.util.function.Predicate
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
import androidx.window.extensions.embedding.ActivityRule
import androidx.window.extensions.embedding.ActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
+import androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams
+import androidx.window.extensions.embedding.AnimationBackground
+import androidx.window.extensions.embedding.AnimationParams
+import androidx.window.extensions.embedding.DividerAttributes
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo
+import androidx.window.extensions.embedding.ParentContainerInfo
import androidx.window.extensions.embedding.SplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.SplitType
+import androidx.window.extensions.embedding.SplitAttributesCalculatorParams
import androidx.window.extensions.embedding.SplitInfo
import androidx.window.extensions.embedding.SplitPairRule
+import androidx.window.extensions.embedding.SplitPinRule
import androidx.window.extensions.embedding.SplitPlaceholderRule
+import androidx.window.extensions.embedding.SplitRule
+import androidx.window.extensions.embedding.WindowAttributes
+import androidx.window.extensions.layout.WindowLayoutInfo
import androidx.window.reflection.ReflectionUtils.doesReturn
import androidx.window.reflection.ReflectionUtils.isPublic
import androidx.window.reflection.ReflectionUtils.validateReflection
import androidx.window.reflection.WindowExtensionsConstants.ACTIVITY_EMBEDDING_COMPONENT_CLASS
+import java.util.concurrent.Executor
/**
* Reflection Guard for [ActivityEmbeddingComponent]. This will go through the
@@ -69,10 +88,13 @@
}
// TODO(b/267573854) : update logic to fallback to lower version
// if higher version is not matched
- return when (ExtensionsUtil.safeVendorApiLevel) {
+ return when (WindowSdkExtensions.getInstance().extensionVersion) {
1 -> hasValidVendorApiLevel1()
- in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
- // TODO(b/267956499) : add hasValidVendorApiLevel3
+ 2 -> hasValidVendorApiLevel2()
+ in 3..4 -> hasValidVendorApiLevel3() // No additional API in 4.
+ 5 -> hasValidVendorApiLevel5()
+ 6 -> hasValidVendorApiLevel6()
+ in 7..Int.MAX_VALUE -> hasValidVendorApiLevel7()
else -> false
}
}
@@ -83,25 +105,34 @@
isActivityEmbeddingComponentValid()
/**
- * [WindowExtensions.VENDOR_API_LEVEL_1] includes the following methods:
+ * Vendor API level 1 includes the following methods:
* - [ActivityEmbeddingComponent.setEmbeddingRules]
* - [ActivityEmbeddingComponent.isActivityEmbedded]
- * - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer] and
- * following classes:
+ * - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer]
+ * - [SplitRule.getSplitRatio]
+ * - [SplitRule.getLayoutDirection] and following classes:
* - [ActivityRule]
+ * - [ActivityRule.Builder]
* - [SplitInfo]
* - [SplitPairRule]
+ * - [SplitPairRule.Builder]
* - [SplitPlaceholderRule]
+ * - [SplitPlaceholderRule.Builder]
*/
@VisibleForTesting
internal fun hasValidVendorApiLevel1(): Boolean {
return isMethodSetEmbeddingRulesValid() &&
isMethodIsActivityEmbeddedValid() &&
isMethodSetSplitInfoCallbackJavaConsumerValid() &&
+ isMethodGetSplitRatioValid() &&
+ isMethodGetLayoutDirectionValid() &&
isClassActivityRuleValid() &&
+ isClassActivityRuleBuilderLevel1Valid() &&
isClassSplitInfoValid() &&
isClassSplitPairRuleValid() &&
- isClassSplitPlaceholderRuleValid()
+ isClassSplitPairRuleBuilderLevel1Valid() &&
+ isClassSplitPlaceholderRuleValid() &&
+ isClassSplitPlaceholderRuleBuilderLevel1Valid()
}
/**
@@ -110,9 +141,16 @@
* - [ActivityEmbeddingComponent.clearSplitInfoCallback]
* - [ActivityEmbeddingComponent.setSplitAttributesCalculator]
* - [ActivityEmbeddingComponent.clearSplitAttributesCalculator]
- * - [SplitInfo.getSplitAttributes] and following classes:
+ * - [SplitInfo.getSplitAttributes]
+ * - [SplitPlaceholderRule.getFinishPrimaryWithPlaceholder]
+ * - [SplitRule.getDefaultSplitAttributes] and following classes:
+ * - [ActivityRule.Builder]
+ * - [EmbeddingRule]
* - [SplitAttributes]
* - [SplitAttributes.SplitType]
+ * - [SplitAttributesCalculatorParams]
+ * - [SplitPairRule.Builder]
+ * - [SplitPlaceholderRule.Builder]
*/
@VisibleForTesting
internal fun hasValidVendorApiLevel2(): Boolean {
@@ -121,10 +159,154 @@
isMethodClearSplitInfoCallbackValid() &&
isMethodSplitAttributesCalculatorValid() &&
isMethodGetSplitAttributesValid() &&
+ isMethodGetFinishPrimaryWithPlaceholderValid() &&
+ isMethodGetDefaultSplitAttributesValid() &&
+ isClassActivityRuleBuilderLevel2Valid() &&
+ isClassEmbeddingRuleValid() &&
isClassSplitAttributesValid() &&
- isClassSplitTypeValid()
+ isClassSplitAttributesCalculatorParamsValid() &&
+ isClassSplitTypeValid() &&
+ isClassSplitPairRuleBuilderLevel2Valid() &&
+ isClassSplitPlaceholderRuleBuilderLevel2Valid()
}
+ /**
+ * Vendor API level 3 includes the following methods:
+ * - [ActivityEmbeddingComponent.updateSplitAttributes]
+ * - [ActivityEmbeddingComponent.invalidateTopVisibleSplitAttributes]
+ * - [SplitInfo.getToken]
+ */
+ @VisibleForTesting
+ internal fun hasValidVendorApiLevel3(): Boolean =
+ hasValidVendorApiLevel2() &&
+ isMethodInvalidateTopVisibleSplitAttributesValid() &&
+ isMethodUpdateSplitAttributesValid() &&
+ isMethodSplitInfoGetTokenValid()
+
+ /**
+ * Vendor API level 5 includes the following methods:
+ * - [ActivityEmbeddingComponent.registerActivityStackCallback]
+ * - [ActivityEmbeddingComponent.unregisterActivityStackCallback]
+ * - [ActivityStack.getActivityStackToken]
+ * - [ActivityStack.Token.createFromBinder]
+ * - [ActivityStack.Token.readFromBundle]
+ * - [ActivityStack.Token.toBundle]
+ * - [ActivityStack.Token.INVALID_ACTIVITY_STACK_TOKEN]
+ * - [AnimationBackground.createColorBackground]
+ * - [AnimationBackground.ANIMATION_BACKGROUND_DEFAULT]
+ * - [SplitAttributes.getAnimationBackground]
+ * - [SplitAttributes.Builder.setAnimationBackground]
+ * - [WindowAttributes.getDimAreaBehavior]
+ * - [SplitAttributes.getWindowAttributes]
+ * - [SplitAttributes.Builder.setWindowAttributes]
+ * - [SplitPinRule.isSticky]
+ * - [ActivityEmbeddingComponent.pinTopActivityStack]
+ * - [ActivityEmbeddingComponent.unpinTopActivityStack]
+ * - [ActivityEmbeddingComponent.updateSplitAttributes] with [SplitInfo.Token]
+ * - [SplitInfo.getSplitInfoToken] and following classes:
+ * - [AnimationBackground]
+ * - [ActivityStack.Token]
+ * - [WindowAttributes]
+ * - [SplitInfo.Token]
+ */
+ @VisibleForTesting
+ internal fun hasValidVendorApiLevel5(): Boolean =
+ hasValidVendorApiLevel3() &&
+ isActivityStackGetActivityStackTokenValid() &&
+ isMethodRegisterActivityStackCallbackValid() &&
+ isMethodUnregisterActivityStackCallbackValid() &&
+ isMethodPinUnpinTopActivityStackValid() &&
+ isMethodUpdateSplitAttributesWithTokenValid() &&
+ isMethodGetSplitInfoTokenValid() &&
+ isClassAnimationBackgroundValid() &&
+ isClassActivityStackTokenValid() &&
+ isClassWindowAttributesValid() &&
+ isClassSplitInfoTokenValid()
+
+ /**
+ * Vendor API level 6 includes the following methods:
+ * - [ActivityEmbeddingComponent.clearEmbeddedActivityWindowInfoCallback]
+ * - [ActivityEmbeddingComponent.getEmbeddedActivityWindowInfo]
+ * - [ActivityEmbeddingComponent.setEmbeddedActivityWindowInfoCallback]
+ * - [SplitAttributes.getDividerAttributes]
+ * - [SplitAttributes.Builder.setDividerAttributes] and following classes:
+ * - [EmbeddedActivityWindowInfo]
+ * - [DividerAttributes]
+ * - [DividerAttributes.Builder]
+ */
+ @VisibleForTesting
+ internal fun hasValidVendorApiLevel6(): Boolean =
+ hasValidVendorApiLevel5() &&
+ isMethodGetEmbeddedActivityWindowInfoValid() &&
+ isMethodSetEmbeddedActivityWindowInfoCallbackValid() &&
+ isMethodClearEmbeddedActivityWindowInfoCallbackValid() &&
+ isMethodGetDividerAttributesValid() &&
+ isMethodSetDividerAttributesValid() &&
+ isClassEmbeddedActivityWindowInfoValid() &&
+ isClassDividerAttributesValid() &&
+ isClassDividerAttributesBuilderValid()
+
+ /**
+ * Vendor API level 7 includes the following methods:
+ * - [SplitAttributes.getAnimationParams]
+ * - [SplitAttributes.Builder.setAnimationParams]
+ * - [DividerAttributes.isDraggingToFullscreenAllowed]
+ * - [DividerAttributes.Builder.setDraggingToFullscreenAllowed] and following classes:
+ * - [AnimationParams]
+ * - [AnimationParams.Builder]
+ */
+ @VisibleForTesting
+ internal fun hasValidVendorApiLevel7(): Boolean =
+ hasValidVendorApiLevel6() &&
+ isMethodGetAnimationParamsValid() &&
+ isMethodSetAnimationParamsValid() &&
+ isMethodIsDraggingToFullscreenAllowedValid() &&
+ isMethodSetDraggingToFullscreenAllowedValid() &&
+ isClassAnimationParamsValid() &&
+ isClassAnimationParamsBuilderValid()
+
+ /**
+ * Overlay features includes the following methods:
+ * - [ActivityEmbeddingComponent.clearActivityStackAttributesCalculator]
+ * - [ActivityEmbeddingComponent.getActivityStackToken]
+ * - [ActivityEmbeddingComponent.getParentContainerInfo]
+ * - [ActivityEmbeddingComponent.setActivityStackAttributesCalculator]
+ * - [ActivityEmbeddingComponent.updateActivityStackAttributes]
+ * - [ActivityStack.getTag] and following classes:
+ * - [ParentContainerInfo]
+ * - [ActivityStackAttributes]
+ * - [ActivityStackAttributes.Builder]
+ * - [ActivityStackAttributesCalculatorParams]
+ */
+ private fun isOverlayFeatureValid(): Boolean =
+ isActivityStackGetTagValid() &&
+ isMethodGetActivityStackTokenValid() &&
+ isClassParentContainerInfoValid() &&
+ isMethodGetParentContainerInfoValid() &&
+ isMethodSetActivityStackAttributesCalculatorValid() &&
+ isMethodClearActivityStackAttributesCalculatorValid() &&
+ isMethodUpdateActivityStackAttributesValid() &&
+ isClassActivityStackAttributesValid() &&
+ isClassActivityStackAttributesBuilderValid() &&
+ isClassActivityStackAttributesCalculatorParamsValid()
+
+ private val activityEmbeddingComponentClass: Class<*>
+ get() {
+ return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
+ }
+
+ private fun isActivityEmbeddingComponentValid(): Boolean {
+ return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
+ val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
+ val getActivityEmbeddingComponentMethod =
+ extensionsClass.getMethod("getActivityEmbeddingComponent")
+ val activityEmbeddingComponentClass = activityEmbeddingComponentClass
+ getActivityEmbeddingComponentMethod.isPublic &&
+ getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
+ }
+ }
+
+ /** Vendor API level 1 validation methods */
private fun isMethodSetEmbeddingRulesValid(): Boolean {
return validateReflection("ActivityEmbeddingComponent#setEmbeddingRules is not valid") {
val setEmbeddingRulesMethod =
@@ -145,6 +327,146 @@
}
}
+ private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
+ return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
+ val consumerClass =
+ consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
+ val setSplitInfoCallbackMethod =
+ activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
+ setSplitInfoCallbackMethod.isPublic
+ }
+ }
+
+ private fun isMethodGetSplitRatioValid(): Boolean =
+ validateReflection("SplitRule#getSplitRatio is not valid") {
+ val splitRuleClass = SplitRule::class.java
+ val getSplitRatioMethod = splitRuleClass.getMethod("getSplitRatio")
+ getSplitRatioMethod.isPublic && getSplitRatioMethod.doesReturn(Float::class.java)
+ }
+
+ private fun isMethodGetLayoutDirectionValid(): Boolean =
+ validateReflection("SplitRule#getLayoutDirection is not valid") {
+ val splitRuleClass = SplitRule::class.java
+ val getLayoutDirectionMethod = splitRuleClass.getMethod("getLayoutDirection")
+ getLayoutDirectionMethod.isPublic &&
+ getLayoutDirectionMethod.doesReturn(Int::class.java)
+ }
+
+ private fun isClassActivityRuleValid(): Boolean =
+ validateReflection("Class ActivityRule is not valid") {
+ val activityRuleClass = ActivityRule::class.java
+ val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
+ shouldAlwaysExpandMethod.isPublic &&
+ shouldAlwaysExpandMethod.doesReturn(Boolean::class.java)
+ }
+
+ private fun isClassActivityRuleBuilderLevel1Valid(): Boolean =
+ validateReflection("Class ActivityRule.Builder is not valid") {
+ val activityRuleBuilderClass = ActivityRule.Builder::class.java
+ val setShouldAlwaysExpandMethod =
+ activityRuleBuilderClass.getMethod("setShouldAlwaysExpand", Boolean::class.java)
+ setShouldAlwaysExpandMethod.isPublic &&
+ setShouldAlwaysExpandMethod.doesReturn(ActivityRule.Builder::class.java)
+ }
+
+ private fun isClassSplitInfoValid(): Boolean =
+ validateReflection("Class SplitInfo is not valid") {
+ val splitInfoClass = SplitInfo::class.java
+ val getPrimaryActivityStackMethod = splitInfoClass.getMethod("getPrimaryActivityStack")
+ val getSecondaryActivityStackMethod =
+ splitInfoClass.getMethod("getSecondaryActivityStack")
+ val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
+ getPrimaryActivityStackMethod.isPublic &&
+ getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
+ getSecondaryActivityStackMethod.isPublic &&
+ getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
+ getSplitRatioMethod.isPublic &&
+ getSplitRatioMethod.doesReturn(Float::class.java)
+ }
+
+ private fun isClassSplitPairRuleValid(): Boolean =
+ validateReflection("Class SplitPairRule is not valid") {
+ val splitPairRuleClass = SplitPairRule::class.java
+ val getFinishPrimaryWithSecondaryMethod =
+ splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
+ val getFinishSecondaryWithPrimaryMethod =
+ splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
+ val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
+ getFinishPrimaryWithSecondaryMethod.isPublic &&
+ getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
+ getFinishSecondaryWithPrimaryMethod.isPublic &&
+ getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
+ shouldClearTopMethod.isPublic &&
+ shouldClearTopMethod.doesReturn(Boolean::class.java)
+ }
+
+ private fun isClassSplitPairRuleBuilderLevel1Valid(): Boolean =
+ validateReflection("Class SplitPairRule.Builder is not valid") {
+ val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
+ val setSplitRatioMethod =
+ splitPairRuleBuilderClass.getMethod("setSplitRatio", Float::class.java)
+ val setLayoutDirectionMethod =
+ splitPairRuleBuilderClass.getMethod("setLayoutDirection", Int::class.java)
+ setSplitRatioMethod.isPublic &&
+ setSplitRatioMethod.doesReturn(SplitPairRule.Builder::class.java) &&
+ setLayoutDirectionMethod.isPublic &&
+ setLayoutDirectionMethod.doesReturn(SplitPairRule.Builder::class.java)
+ }
+
+ private fun isClassSplitPlaceholderRuleValid(): Boolean =
+ validateReflection("Class SplitPlaceholderRule is not valid") {
+ val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
+ val getPlaceholderIntentMethod =
+ splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
+ val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
+ val getFinishPrimaryWithSecondaryMethod =
+ splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
+ getPlaceholderIntentMethod.isPublic &&
+ getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
+ isStickyMethod.isPublic &&
+ isStickyMethod.doesReturn(Boolean::class.java) &&
+ getFinishPrimaryWithSecondaryMethod.isPublic &&
+ getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
+ }
+
+ private fun isClassSplitPlaceholderRuleBuilderLevel1Valid(): Boolean =
+ validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
+ val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
+ val setSplitRatioMethod =
+ splitPlaceholderRuleBuilderClass.getMethod("setSplitRatio", Float::class.java)
+ val setLayoutDirectionMethod =
+ splitPlaceholderRuleBuilderClass.getMethod("setLayoutDirection", Int::class.java)
+ val setStickyMethod =
+ splitPlaceholderRuleBuilderClass.getMethod("setSticky", Boolean::class.java)
+ val setFinishPrimaryWithSecondaryMethod =
+ splitPlaceholderRuleBuilderClass.getMethod(
+ "setFinishPrimaryWithSecondary",
+ Int::class.java
+ )
+ setSplitRatioMethod.isPublic &&
+ setSplitRatioMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+ setLayoutDirectionMethod.isPublic &&
+ setLayoutDirectionMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+ setStickyMethod.isPublic &&
+ setStickyMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+ setFinishPrimaryWithSecondaryMethod.isPublic &&
+ setFinishPrimaryWithSecondaryMethod.doesReturn(
+ SplitPlaceholderRule.Builder::class.java
+ )
+ }
+
+ /** Vendor API level 2 validation methods */
+ private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
+ return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
+ val setSplitInfoCallbackMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "setSplitInfoCallback",
+ Consumer::class.java
+ )
+ setSplitInfoCallbackMethod.isPublic
+ }
+ }
+
private fun isMethodClearSplitInfoCallbackValid(): Boolean {
return validateReflection(
"ActivityEmbeddingComponent#clearSplitInfoCallback is not valid"
@@ -179,6 +501,45 @@
getSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
}
+ private fun isMethodGetFinishPrimaryWithPlaceholderValid(): Boolean =
+ validateReflection("SplitPlaceholderRule#getFinishPrimaryWithPlaceholder is not valid") {
+ val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
+ val getFinishPrimaryWithPlaceholderMethod =
+ splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithPlaceholder")
+ getFinishPrimaryWithPlaceholderMethod.isPublic &&
+ getFinishPrimaryWithPlaceholderMethod.doesReturn(Int::class.java)
+ }
+
+ private fun isMethodGetDefaultSplitAttributesValid(): Boolean =
+ validateReflection("SplitRule#getDefaultSplitAttributes is not valid") {
+ val splitRuleClass = SplitRule::class.java
+ val getDefaultSplitAttributesMethod =
+ splitRuleClass.getMethod("getDefaultSplitAttributes")
+ getDefaultSplitAttributesMethod.isPublic &&
+ getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
+ }
+
+ private fun isClassActivityRuleBuilderLevel2Valid(): Boolean =
+ validateReflection("Class ActivityRule.Builder is not valid") {
+ val activityRuleBuilderClass = ActivityRule.Builder::class.java
+ val activityRuleBuilderConstructor =
+ activityRuleBuilderClass.getDeclaredConstructor(
+ Predicate::class.java,
+ Predicate::class.java
+ )
+ val setTagMethod = activityRuleBuilderClass.getMethod("setTag", String::class.java)
+ activityRuleBuilderConstructor.isPublic &&
+ setTagMethod.isPublic &&
+ setTagMethod.doesReturn(ActivityRule.Builder::class.java)
+ }
+
+ private fun isClassEmbeddingRuleValid(): Boolean =
+ validateReflection("Class EmbeddingRule is not valid") {
+ val embeddingRuleClass = EmbeddingRule::class.java
+ val getTagMethod = embeddingRuleClass.getMethod("getTag")
+ getTagMethod.isPublic && getTagMethod.doesReturn(String::class.java)
+ }
+
private fun isClassSplitAttributesValid(): Boolean =
validateReflection("Class SplitAttributes is not valid") {
val splitAttributesClass = SplitAttributes::class.java
@@ -197,6 +558,36 @@
setLayoutDirectionMethod.isPublic
}
+ @Suppress("newApi") // Suppress lint check for WindowMetrics
+ private fun isClassSplitAttributesCalculatorParamsValid(): Boolean =
+ validateReflection("Class SplitAttributesCalculatorParams is not valid") {
+ val splitAttributesCalculatorParamsClass = SplitAttributesCalculatorParams::class.java
+ val getParentWindowMetricsMethod =
+ splitAttributesCalculatorParamsClass.getMethod("getParentWindowMetrics")
+ val getParentConfigurationMethod =
+ splitAttributesCalculatorParamsClass.getMethod("getParentConfiguration")
+ val getDefaultSplitAttributesMethod =
+ splitAttributesCalculatorParamsClass.getMethod("getDefaultSplitAttributes")
+ val areDefaultConstraintsSatisfiedMethod =
+ splitAttributesCalculatorParamsClass.getMethod("areDefaultConstraintsSatisfied")
+ val getParentWindowLayoutInfoMethod =
+ splitAttributesCalculatorParamsClass.getMethod("getParentWindowLayoutInfo")
+ val getSplitRuleTagMethod =
+ splitAttributesCalculatorParamsClass.getMethod("getSplitRuleTag")
+ getParentWindowMetricsMethod.isPublic &&
+ getParentWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
+ getParentConfigurationMethod.isPublic &&
+ getParentConfigurationMethod.doesReturn(Configuration::class.java) &&
+ getDefaultSplitAttributesMethod.isPublic &&
+ getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java) &&
+ areDefaultConstraintsSatisfiedMethod.isPublic &&
+ areDefaultConstraintsSatisfiedMethod.doesReturn(Boolean::class.java) &&
+ getParentWindowLayoutInfoMethod.isPublic &&
+ getParentWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java) &&
+ getSplitRuleTagMethod.isPublic &&
+ getSplitRuleTagMethod.doesReturn(String::class.java)
+ }
+
private fun isClassSplitTypeValid(): Boolean =
validateReflection("Class SplitAttributes.SplitType is not valid") {
val ratioSplitTypeClass = SplitType.RatioSplitType::class.java
@@ -222,99 +613,577 @@
expandContainersSplitTypeConstructor.isPublic
}
- private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
- return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
- val consumerClass =
- consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
- val setSplitInfoCallbackMethod =
- activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
- setSplitInfoCallbackMethod.isPublic
- }
- }
-
- private fun isClassActivityRuleValid(): Boolean =
- validateReflection("Class ActivityRule is not valid") {
- val activityRuleClass = ActivityRule::class.java
- val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
- val activityRuleBuilderClass = ActivityRule.Builder::class.java
- val setShouldAlwaysExpandMethod =
- activityRuleBuilderClass.getMethod("setShouldAlwaysExpand", Boolean::class.java)
- shouldAlwaysExpandMethod.isPublic &&
- shouldAlwaysExpandMethod.doesReturn(Boolean::class.java) &&
- setShouldAlwaysExpandMethod.isPublic
+ private fun isClassSplitPairRuleBuilderLevel2Valid(): Boolean =
+ validateReflection("Class SplitPairRule.Builder is not valid") {
+ val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
+ val splitPairRuleBuilderConstructor =
+ splitPairRuleBuilderClass.getDeclaredConstructor(
+ Predicate::class.java,
+ Predicate::class.java,
+ Predicate::class.java
+ )
+ val setDefaultSplitAttributesMethod =
+ splitPairRuleBuilderClass.getMethod(
+ "setDefaultSplitAttributes",
+ SplitAttributes::class.java,
+ )
+ val setTagMethod = splitPairRuleBuilderClass.getMethod("setTag", String::class.java)
+ splitPairRuleBuilderConstructor.isPublic &&
+ setDefaultSplitAttributesMethod.isPublic &&
+ setDefaultSplitAttributesMethod.doesReturn(SplitPairRule.Builder::class.java) &&
+ setTagMethod.isPublic &&
+ setTagMethod.doesReturn(SplitPairRule.Builder::class.java)
}
- private fun isClassSplitInfoValid(): Boolean =
- validateReflection("Class SplitInfo is not valid") {
- val splitInfoClass = SplitInfo::class.java
- val getPrimaryActivityStackMethod = splitInfoClass.getMethod("getPrimaryActivityStack")
- val getSecondaryActivityStackMethod =
- splitInfoClass.getMethod("getSecondaryActivityStack")
- val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
- getPrimaryActivityStackMethod.isPublic &&
- getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
- getSecondaryActivityStackMethod.isPublic &&
- getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
- getSplitRatioMethod.isPublic &&
- getSplitRatioMethod.doesReturn(Float::class.java)
+ private fun isClassSplitPlaceholderRuleBuilderLevel2Valid(): Boolean =
+ validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
+ val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
+ val splitPlaceholderRuleBuilderConstructor =
+ splitPlaceholderRuleBuilderClass.getDeclaredConstructor(
+ Intent::class.java,
+ Predicate::class.java,
+ Predicate::class.java,
+ Predicate::class.java
+ )
+ val setDefaultSplitAttributesMethod =
+ splitPlaceholderRuleBuilderClass.getMethod(
+ "setDefaultSplitAttributes",
+ SplitAttributes::class.java,
+ )
+ val setFinishPrimaryWithPlaceholderMethod =
+ splitPlaceholderRuleBuilderClass.getMethod(
+ "setFinishPrimaryWithPlaceholder",
+ Int::class.java
+ )
+ val setTagMethod =
+ splitPlaceholderRuleBuilderClass.getMethod("setTag", String::class.java)
+ splitPlaceholderRuleBuilderConstructor.isPublic &&
+ setDefaultSplitAttributesMethod.isPublic &&
+ setDefaultSplitAttributesMethod.doesReturn(
+ SplitPlaceholderRule.Builder::class.java
+ ) &&
+ setFinishPrimaryWithPlaceholderMethod.isPublic &&
+ setFinishPrimaryWithPlaceholderMethod.doesReturn(
+ SplitPlaceholderRule.Builder::class.java
+ ) &&
+ setTagMethod.isPublic &&
+ setTagMethod.doesReturn(SplitPlaceholderRule.Builder::class.java)
}
- private fun isClassSplitPairRuleValid(): Boolean =
- validateReflection("Class SplitPairRule is not valid") {
- val splitPairRuleClass = SplitPairRule::class.java
- val getFinishPrimaryWithSecondaryMethod =
- splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
- val getFinishSecondaryWithPrimaryMethod =
- splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
- val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
- getFinishPrimaryWithSecondaryMethod.isPublic &&
- getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
- getFinishSecondaryWithPrimaryMethod.isPublic &&
- getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
- shouldClearTopMethod.isPublic &&
- shouldClearTopMethod.doesReturn(Boolean::class.java)
+ /** Vendor API level 3 validation methods */
+ private fun isMethodInvalidateTopVisibleSplitAttributesValid(): Boolean =
+ validateReflection("#invalidateTopVisibleSplitAttributes is not valid") {
+ val invalidateTopVisibleSplitAttributesMethod =
+ activityEmbeddingComponentClass.getMethod("invalidateTopVisibleSplitAttributes")
+ invalidateTopVisibleSplitAttributesMethod.isPublic
}
- private fun isClassSplitPlaceholderRuleValid(): Boolean =
- validateReflection("Class SplitPlaceholderRule is not valid") {
- val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
- val getPlaceholderIntentMethod =
- splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
- val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
- val getFinishPrimaryWithSecondaryMethod =
- splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
- getPlaceholderIntentMethod.isPublic &&
- getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
- isStickyMethod.isPublic &&
- isStickyMethod.doesReturn(Boolean::class.java)
- getFinishPrimaryWithSecondaryMethod.isPublic &&
- getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
- }
-
- private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
- return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
- val setSplitInfoCallbackMethod =
+ private fun isMethodUpdateSplitAttributesValid(): Boolean =
+ validateReflection("#updateSplitAttributes is not valid") {
+ val updateSplitAttributesMethod =
activityEmbeddingComponentClass.getMethod(
- "setSplitInfoCallback",
+ "updateSplitAttributes",
+ IBinder::class.java,
+ SplitAttributes::class.java
+ )
+ updateSplitAttributesMethod.isPublic
+ }
+
+ private fun isMethodSplitInfoGetTokenValid(): Boolean =
+ validateReflection("SplitInfo#getToken is not valid") {
+ val splitInfoClass = SplitInfo::class.java
+ val getTokenMethod = splitInfoClass.getMethod("getToken")
+ getTokenMethod.isPublic && getTokenMethod.doesReturn(IBinder::class.java)
+ }
+
+ /** Vendor API level 5 validation methods */
+ private fun isActivityStackGetActivityStackTokenValid(): Boolean =
+ validateReflection("ActivityStack#getActivityToken is not valid") {
+ val activityStackClass = ActivityStack::class.java
+ val getActivityStackTokenMethod = activityStackClass.getMethod("getActivityStackToken")
+
+ getActivityStackTokenMethod.isPublic &&
+ getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
+ }
+
+ private fun isMethodRegisterActivityStackCallbackValid(): Boolean =
+ validateReflection("registerActivityStackCallback is not valid") {
+ val registerActivityStackCallbackMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "registerActivityStackCallback",
+ Executor::class.java,
Consumer::class.java
)
- setSplitInfoCallbackMethod.isPublic
+ registerActivityStackCallbackMethod.isPublic
}
- }
- private fun isActivityEmbeddingComponentValid(): Boolean {
- return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
- val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
- val getActivityEmbeddingComponentMethod =
- extensionsClass.getMethod("getActivityEmbeddingComponent")
- val activityEmbeddingComponentClass = activityEmbeddingComponentClass
- getActivityEmbeddingComponentMethod.isPublic &&
- getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
+ private fun isMethodUnregisterActivityStackCallbackValid(): Boolean =
+ validateReflection("unregisterActivityStackCallback is not valid") {
+ val unregisterActivityStackCallbackMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "unregisterActivityStackCallback",
+ Consumer::class.java
+ )
+ unregisterActivityStackCallbackMethod.isPublic
}
- }
- private val activityEmbeddingComponentClass: Class<*>
- get() {
- return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
+ private fun isMethodPinUnpinTopActivityStackValid(): Boolean =
+ validateReflection("#pin(unPin)TopActivityStack is not valid") {
+ val splitPinRuleClass = SplitPinRule::class.java
+ val isStickyMethod = splitPinRuleClass.getMethod("isSticky")
+ val pinTopActivityStackMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "pinTopActivityStack",
+ Int::class.java,
+ SplitPinRule::class.java
+ )
+ val unpinTopActivityStackMethod =
+ activityEmbeddingComponentClass.getMethod("unpinTopActivityStack", Int::class.java)
+ isStickyMethod.isPublic &&
+ isStickyMethod.doesReturn(Boolean::class.java) &&
+ pinTopActivityStackMethod.isPublic &&
+ pinTopActivityStackMethod.doesReturn(Boolean::class.java) &&
+ unpinTopActivityStackMethod.isPublic
+ }
+
+ private fun isMethodUpdateSplitAttributesWithTokenValid(): Boolean =
+ validateReflection("updateSplitAttributes is not valid") {
+ val updateSplitAttributesMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "updateSplitAttributes",
+ SplitInfo.Token::class.java,
+ SplitAttributes::class.java,
+ )
+ updateSplitAttributesMethod.isPublic
+ }
+
+ private fun isMethodGetSplitInfoTokenValid(): Boolean =
+ validateReflection("SplitInfo#getSplitInfoToken is not valid") {
+ val splitInfoClass = SplitInfo::class.java
+ val getSplitInfoToken = splitInfoClass.getMethod("getSplitInfoToken")
+ getSplitInfoToken.isPublic && getSplitInfoToken.doesReturn(SplitInfo.Token::class.java)
+ }
+
+ private fun isClassAnimationBackgroundValid(): Boolean =
+ validateReflection("Class AnimationBackground is not valid") {
+ val animationBackgroundClass = AnimationBackground::class.java
+ val colorBackgroundClass = AnimationBackground.ColorBackground::class.java
+ val createColorBackgroundMethod =
+ animationBackgroundClass.getMethod("createColorBackground", Int::class.java)
+ val animationBackgroundDefaultField =
+ animationBackgroundClass.getDeclaredField("ANIMATION_BACKGROUND_DEFAULT")
+ val colorBackgroundGetColor = colorBackgroundClass.getMethod("getColor")
+
+ val splitAttributesClass = SplitAttributes::class.java
+ val getAnimationBackgroundMethod =
+ splitAttributesClass.getMethod("getAnimationBackground")
+
+ val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+ val setAnimationBackgroundMethod =
+ splitAttributesBuilderClass.getMethod(
+ "setAnimationBackground",
+ AnimationBackground::class.java
+ )
+
+ createColorBackgroundMethod.isPublic &&
+ createColorBackgroundMethod.doesReturn(colorBackgroundClass) &&
+ animationBackgroundDefaultField.isPublic &&
+ colorBackgroundGetColor.isPublic &&
+ colorBackgroundGetColor.doesReturn(Int::class.java) &&
+ getAnimationBackgroundMethod.isPublic &&
+ getAnimationBackgroundMethod.doesReturn(animationBackgroundClass) &&
+ setAnimationBackgroundMethod.isPublic &&
+ setAnimationBackgroundMethod.doesReturn(SplitAttributes.Builder::class.java)
+ }
+
+ private fun isClassActivityStackTokenValid(): Boolean =
+ validateReflection("Class ActivityStack.Token is not valid") {
+ val activityStackTokenClass = ActivityStack.Token::class.java
+ val toBundleMethod = activityStackTokenClass.getMethod("toBundle")
+ val readFromBundle =
+ activityStackTokenClass.getMethod("readFromBundle", Bundle::class.java)
+ val createFromBinder =
+ activityStackTokenClass.getMethod("createFromBinder", IBinder::class.java)
+ val invalidActivityStackTokenField =
+ activityStackTokenClass.getDeclaredField("INVALID_ACTIVITY_STACK_TOKEN")
+
+ toBundleMethod.isPublic &&
+ toBundleMethod.doesReturn(Bundle::class.java) &&
+ readFromBundle.isPublic &&
+ readFromBundle.doesReturn(activityStackTokenClass) &&
+ createFromBinder.isPublic &&
+ createFromBinder.doesReturn(activityStackTokenClass) &&
+ invalidActivityStackTokenField.isPublic
+ }
+
+ private fun isClassWindowAttributesValid(): Boolean =
+ validateReflection("Class WindowAttributes is not valid") {
+ val windowAttributesClass = WindowAttributes::class.java
+ val getDimAreaBehaviorMethod = windowAttributesClass.getMethod("getDimAreaBehavior")
+
+ val splitAttributesClass = SplitAttributes::class.java
+ val getWindowAttributesMethod = splitAttributesClass.getMethod("getWindowAttributes")
+
+ val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+ val setWindowAttributesMethod =
+ splitAttributesBuilderClass.getMethod(
+ "setWindowAttributes",
+ WindowAttributes::class.java
+ )
+
+ getDimAreaBehaviorMethod.isPublic &&
+ getDimAreaBehaviorMethod.doesReturn(Int::class.java) &&
+ getWindowAttributesMethod.isPublic &&
+ getWindowAttributesMethod.doesReturn(windowAttributesClass) &&
+ setWindowAttributesMethod.isPublic &&
+ setWindowAttributesMethod.doesReturn(SplitAttributes.Builder::class.java)
+ }
+
+ private fun isClassSplitInfoTokenValid(): Boolean =
+ validateReflection("SplitInfo.Token is not valid") {
+ val splitInfoTokenClass = SplitInfo.Token::class.java
+ val createFromBinder =
+ splitInfoTokenClass.getMethod("createFromBinder", IBinder::class.java)
+
+ createFromBinder.isPublic && createFromBinder.doesReturn(splitInfoTokenClass)
+ }
+
+ /** Vendor API level 6 validation methods */
+ private fun isMethodGetEmbeddedActivityWindowInfoValid(): Boolean =
+ validateReflection(
+ "ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo is not valid"
+ ) {
+ val getEmbeddedActivityWindowInfoMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "getEmbeddedActivityWindowInfo",
+ Activity::class.java
+ )
+ getEmbeddedActivityWindowInfoMethod.isPublic &&
+ getEmbeddedActivityWindowInfoMethod.doesReturn(
+ EmbeddedActivityWindowInfo::class.java
+ )
+ }
+
+ private fun isMethodSetEmbeddedActivityWindowInfoCallbackValid(): Boolean =
+ validateReflection(
+ "ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback is not valid"
+ ) {
+ val setEmbeddedActivityWindowInfoCallbackMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "setEmbeddedActivityWindowInfoCallback",
+ Executor::class.java,
+ Consumer::class.java
+ )
+ setEmbeddedActivityWindowInfoCallbackMethod.isPublic
+ }
+
+ private fun isMethodClearEmbeddedActivityWindowInfoCallbackValid(): Boolean =
+ validateReflection(
+ "ActivityEmbeddingComponent#clearEmbeddedActivityWindowInfoCallback is not valid"
+ ) {
+ val clearEmbeddedActivityWindowInfoCallbackMethod =
+ activityEmbeddingComponentClass.getMethod("clearEmbeddedActivityWindowInfoCallback")
+ clearEmbeddedActivityWindowInfoCallbackMethod.isPublic
+ }
+
+ private fun isMethodGetDividerAttributesValid(): Boolean =
+ validateReflection("SplitAttributes#getDividerAttributes is not valid") {
+ val splitAttributesClass = SplitAttributes::class.java
+ val getDividerAttributesMethod = splitAttributesClass.getMethod("getDividerAttributes")
+ getDividerAttributesMethod.isPublic &&
+ getDividerAttributesMethod.doesReturn(DividerAttributes::class.java)
+ }
+
+ private fun isMethodSetDividerAttributesValid(): Boolean =
+ validateReflection("SplitAttributes#setDividerAttributes is not valid") {
+ val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+ val setDividerAttributesMethod =
+ splitAttributesBuilderClass.getMethod(
+ "setDividerAttributes",
+ DividerAttributes::class.java
+ )
+ setDividerAttributesMethod.isPublic &&
+ setDividerAttributesMethod.doesReturn(SplitAttributes.Builder::class.java)
+ }
+
+ private fun isClassEmbeddedActivityWindowInfoValid(): Boolean =
+ validateReflection("Class EmbeddedActivityWindowInfo is not valid") {
+ val embeddedActivityWindowInfoClass = EmbeddedActivityWindowInfo::class.java
+ val getActivityMethod = embeddedActivityWindowInfoClass.getMethod("getActivity")
+ val isEmbeddedMethod = embeddedActivityWindowInfoClass.getMethod("isEmbedded")
+ val getTaskBoundsMethod = embeddedActivityWindowInfoClass.getMethod("getTaskBounds")
+ val getActivityStackBoundsMethod =
+ embeddedActivityWindowInfoClass.getMethod("getActivityStackBounds")
+ getActivityMethod.isPublic &&
+ getActivityMethod.doesReturn(Activity::class.java) &&
+ isEmbeddedMethod.isPublic &&
+ isEmbeddedMethod.doesReturn(Boolean::class.java) &&
+ getTaskBoundsMethod.isPublic &&
+ getTaskBoundsMethod.doesReturn(Rect::class.java) &&
+ getActivityStackBoundsMethod.isPublic &&
+ getActivityStackBoundsMethod.doesReturn(Rect::class.java)
+ }
+
+ private fun isClassDividerAttributesValid(): Boolean =
+ validateReflection("Class DividerAttributes is not valid") {
+ val dividerAttributesClass = DividerAttributes::class.java
+ val getDividerTypeMethod = dividerAttributesClass.getMethod("getDividerType")
+ val getWidthDpMethod = dividerAttributesClass.getMethod("getWidthDp")
+ val getPrimaryMinRatioMethod = dividerAttributesClass.getMethod("getPrimaryMinRatio")
+ val getPrimaryMaxRatioMethod = dividerAttributesClass.getMethod("getPrimaryMaxRatio")
+ val getDividerColorMethod = dividerAttributesClass.getMethod("getDividerColor")
+ getDividerTypeMethod.isPublic &&
+ getDividerTypeMethod.doesReturn(Int::class.java) &&
+ getWidthDpMethod.isPublic &&
+ getWidthDpMethod.doesReturn(Int::class.java) &&
+ getPrimaryMinRatioMethod.isPublic &&
+ getPrimaryMinRatioMethod.doesReturn(Float::class.java) &&
+ getPrimaryMaxRatioMethod.isPublic &&
+ getPrimaryMaxRatioMethod.doesReturn(Float::class.java) &&
+ getDividerColorMethod.isPublic &&
+ getDividerColorMethod.doesReturn(Int::class.java)
+ }
+
+ private fun isClassDividerAttributesBuilderValid(): Boolean =
+ validateReflection("Class DividerAttributes.Builder is not valid") {
+ val dividerAttributesBuilderClass = DividerAttributes.Builder::class.java
+ val dividerAttributesTypeBuilderConstructor =
+ dividerAttributesBuilderClass.getDeclaredConstructor(Int::class.java)
+ val dividerAttributesBuilderConstructor =
+ dividerAttributesBuilderClass.getDeclaredConstructor(DividerAttributes::class.java)
+ val setWidthDpMethod =
+ dividerAttributesBuilderClass.getMethod("setWidthDp", Int::class.java)
+ val setPrimaryMinRatioMethod =
+ dividerAttributesBuilderClass.getMethod("setPrimaryMinRatio", Float::class.java)
+ val setPrimaryMaxRatioMethod =
+ dividerAttributesBuilderClass.getMethod("setPrimaryMaxRatio", Float::class.java)
+ val setDividerColorMethod =
+ dividerAttributesBuilderClass.getMethod("setDividerColor", Int::class.java)
+ dividerAttributesTypeBuilderConstructor.isPublic &&
+ dividerAttributesBuilderConstructor.isPublic &&
+ setWidthDpMethod.isPublic &&
+ setWidthDpMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+ setPrimaryMinRatioMethod.isPublic &&
+ setPrimaryMinRatioMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+ setPrimaryMaxRatioMethod.isPublic &&
+ setPrimaryMaxRatioMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+ setDividerColorMethod.isPublic &&
+ setDividerColorMethod.doesReturn(DividerAttributes.Builder::class.java)
+ }
+
+ /** Vendor API level 7 validation methods */
+ private fun isMethodGetAnimationParamsValid(): Boolean =
+ validateReflection("SplitAttributes#getAnimationParams is not valid") {
+ val splitAttributesClass = SplitAttributes::class.java
+ val getAnimationParamsMethod = splitAttributesClass.getMethod("getAnimationParams")
+ getAnimationParamsMethod.isPublic &&
+ getAnimationParamsMethod.doesReturn(AnimationParams::class.java)
+ }
+
+ private fun isMethodSetAnimationParamsValid(): Boolean =
+ validateReflection("SplitAttributes#setAnimationParams is not valid") {
+ val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+ val setAnimationParamsMethod =
+ splitAttributesBuilderClass.getMethod(
+ "setAnimationParams",
+ AnimationParams::class.java
+ )
+ setAnimationParamsMethod.isPublic &&
+ setAnimationParamsMethod.doesReturn(SplitAttributes.Builder::class.java)
+ }
+
+ private fun isMethodIsDraggingToFullscreenAllowedValid(): Boolean =
+ validateReflection("DividerAttributes#isDraggingToFullscreenAllowed is not valid") {
+ val dividerAttributesClass = DividerAttributes::class.java
+ val getDividerTypeMethod =
+ dividerAttributesClass.getMethod("isDraggingToFullscreenAllowed")
+ getDividerTypeMethod.isPublic && getDividerTypeMethod.doesReturn(Boolean::class.java)
+ }
+
+ private fun isMethodSetDraggingToFullscreenAllowedValid(): Boolean =
+ validateReflection(
+ "DividerAttributes.Builder#setDraggingToFullscreenAllowed is not valid"
+ ) {
+ val dividerAttributesBuilderClass = DividerAttributes.Builder::class.java
+ val setDividerColorMethod =
+ dividerAttributesBuilderClass.getMethod(
+ "setDraggingToFullscreenAllowed",
+ Boolean::class.java
+ )
+ setDividerColorMethod.isPublic &&
+ setDividerColorMethod.doesReturn(DividerAttributes.Builder::class.java)
+ }
+
+ private fun isClassAnimationParamsValid(): Boolean =
+ validateReflection("Class AnimationParams is not valid") {
+ val animationParamsClass = AnimationParams::class.java
+ val animationResourcesIdDefaultField =
+ animationParamsClass.getDeclaredField("DEFAULT_ANIMATION_RESOURCES_ID")
+ val getAnimationBackgroundMethod =
+ animationParamsClass.getMethod("getAnimationBackground")
+ val getOpenAnimationResIdMethod =
+ animationParamsClass.getMethod("getOpenAnimationResId")
+ val getCloseAnimationResIdMethod =
+ animationParamsClass.getMethod("getCloseAnimationResId")
+ val getChangeAnimationResIdMethod =
+ animationParamsClass.getMethod("getChangeAnimationResId")
+ animationResourcesIdDefaultField.isPublic &&
+ getAnimationBackgroundMethod.isPublic &&
+ getAnimationBackgroundMethod.doesReturn(AnimationBackground::class.java) &&
+ getOpenAnimationResIdMethod.isPublic &&
+ getOpenAnimationResIdMethod.doesReturn(Int::class.java) &&
+ getCloseAnimationResIdMethod.isPublic &&
+ getCloseAnimationResIdMethod.doesReturn(Int::class.java) &&
+ getChangeAnimationResIdMethod.isPublic &&
+ getChangeAnimationResIdMethod.doesReturn(Int::class.java)
+ }
+
+ private fun isClassAnimationParamsBuilderValid(): Boolean =
+ validateReflection("Class AnimationParams.Builder is not valid") {
+ val animationParamsBuilderClass = AnimationParams.Builder::class.java
+ val setAnimationBackgroundMethod =
+ animationParamsBuilderClass.getMethod(
+ "setAnimationBackground",
+ AnimationBackground::class.java
+ )
+ val setOpenAnimationResIdMethod =
+ animationParamsBuilderClass.getMethod("setOpenAnimationResId", Int::class.java)
+ val setCloseAnimationResIdMethod =
+ animationParamsBuilderClass.getMethod("setCloseAnimationResId", Int::class.java)
+ val setChangeAnimationResIdMethod =
+ animationParamsBuilderClass.getMethod("setChangeAnimationResId", Int::class.java)
+ setAnimationBackgroundMethod.isPublic &&
+ setAnimationBackgroundMethod.doesReturn(AnimationParams.Builder::class.java) &&
+ setOpenAnimationResIdMethod.isPublic &&
+ setOpenAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java) &&
+ setCloseAnimationResIdMethod.isPublic &&
+ setCloseAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java) &&
+ setChangeAnimationResIdMethod.isPublic &&
+ setChangeAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java)
+ }
+
+ /** Overlay features validation methods */
+ private fun isActivityStackGetTagValid(): Boolean =
+ validateReflection("ActivityStack#getTag is not valid") {
+ val activityStackClass = ActivityStack::class.java
+ val getTokenMethod = activityStackClass.getMethod("getTag")
+
+ getTokenMethod.isPublic && getTokenMethod.doesReturn(String::class.java)
+ }
+
+ private fun isMethodGetActivityStackTokenValid(): Boolean =
+ validateReflection("getActivityStackToken is not valid") {
+ val getActivityStackTokenMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "getActivityStackToken",
+ String::class.java
+ )
+ getActivityStackTokenMethod.isPublic &&
+ getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
+ }
+
+ @Suppress("newApi") // Suppress lint check for WindowMetrics
+ private fun isClassParentContainerInfoValid(): Boolean =
+ validateReflection("ParentContainerInfo is not valid") {
+ val parentContainerInfoClass = ParentContainerInfo::class.java
+ val getWindowMetricsMethod = parentContainerInfoClass.getMethod("getWindowMetrics")
+ val getConfigurationMethod = parentContainerInfoClass.getMethod("getConfiguration")
+ val getWindowLayoutInfoMethod =
+ parentContainerInfoClass.getMethod("getWindowLayoutInfo")
+ getWindowMetricsMethod.isPublic &&
+ getWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
+ getConfigurationMethod.isPublic &&
+ getConfigurationMethod.doesReturn(Configuration::class.java) &&
+ getWindowLayoutInfoMethod.isPublic &&
+ getWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java)
+ }
+
+ private fun isMethodGetParentContainerInfoValid(): Boolean =
+ validateReflection("ActivityEmbeddingComponent#getParentContainerInfo is not valid") {
+ val getParentContainerInfoMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "getParentContainerInfo",
+ ActivityStack.Token::class.java
+ )
+ getParentContainerInfoMethod.isPublic &&
+ getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java)
+ }
+
+ private fun isMethodSetActivityStackAttributesCalculatorValid(): Boolean =
+ validateReflection("setActivityStackAttributesCalculator is not valid") {
+ val setActivityStackAttributesCalculatorMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "setActivityStackAttributesCalculator",
+ Function::class.java
+ )
+ setActivityStackAttributesCalculatorMethod.isPublic
+ }
+
+ private fun isMethodClearActivityStackAttributesCalculatorValid(): Boolean =
+ validateReflection("clearActivityStackAttributesCalculator is not valid") {
+ val setActivityStackAttributesCalculatorMethod =
+ activityEmbeddingComponentClass.getMethod("clearActivityStackAttributesCalculator")
+ setActivityStackAttributesCalculatorMethod.isPublic
+ }
+
+ private fun isMethodUpdateActivityStackAttributesValid(): Boolean =
+ validateReflection("updateActivityStackAttributes is not valid") {
+ val updateActivityStackAttributesMethod =
+ activityEmbeddingComponentClass.getMethod(
+ "updateActivityStackAttributes",
+ ActivityStack.Token::class.java,
+ ActivityStackAttributes::class.java
+ )
+ updateActivityStackAttributesMethod.isPublic
+ }
+
+ private fun isClassActivityStackAttributesValid(): Boolean =
+ validateReflection("Class ActivityStackAttributes is not valid") {
+ val activityStackAttributesClass = ActivityStackAttributes::class.java
+ val getRelativeBoundsMethod =
+ activityStackAttributesClass.getMethod("getRelativeBounds")
+ val getWindowAttributesMethod =
+ activityStackAttributesClass.getMethod("getWindowAttributes")
+ getRelativeBoundsMethod.isPublic &&
+ getRelativeBoundsMethod.doesReturn(Rect::class.java) &&
+ getWindowAttributesMethod.isPublic &&
+ getWindowAttributesMethod.doesReturn(WindowAttributes::class.java)
+ }
+
+ private fun isClassActivityStackAttributesBuilderValid(): Boolean =
+ validateReflection("Class ActivityStackAttributes.Builder is not valid") {
+ val activityStackAttributesBuilderClass = ActivityStackAttributes.Builder::class.java
+ val activityStackAttributesBuilderConstructor =
+ activityStackAttributesBuilderClass.getDeclaredConstructor()
+ val setRelativeBoundsMethod =
+ activityStackAttributesBuilderClass.getMethod("setRelativeBounds", Rect::class.java)
+ val setWindowAttributesMethod =
+ activityStackAttributesBuilderClass.getMethod(
+ "setWindowAttributes",
+ WindowAttributes::class.java
+ )
+ activityStackAttributesBuilderConstructor.isPublic &&
+ setRelativeBoundsMethod.isPublic &&
+ setRelativeBoundsMethod.doesReturn(ActivityStackAttributes.Builder::class.java) &&
+ setWindowAttributesMethod.isPublic &&
+ setWindowAttributesMethod.doesReturn(ActivityStackAttributes.Builder::class.java)
+ }
+
+ private fun isClassActivityStackAttributesCalculatorParamsValid(): Boolean =
+ validateReflection("Class ActivityStackAttributesCalculatorParams is not valid") {
+ val activityStackAttributesCalculatorParamsClass =
+ ActivityStackAttributesCalculatorParams::class.java
+ val getParentContainerInfoMethod =
+ activityStackAttributesCalculatorParamsClass.getMethod("getParentContainerInfo")
+ val getActivityStackTagMethod =
+ activityStackAttributesCalculatorParamsClass.getMethod("getActivityStackTag")
+ val getLaunchOptionsMethod =
+ activityStackAttributesCalculatorParamsClass.getMethod("getLaunchOptions")
+ getParentContainerInfoMethod.isPublic &&
+ getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java) &&
+ getActivityStackTagMethod.isPublic &&
+ getActivityStackTagMethod.doesReturn(String::class.java) &&
+ getLaunchOptionsMethod.isPublic &&
+ getLaunchOptionsMethod.doesReturn(Bundle::class.java)
}
}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index 5b79c82..2d63e8a 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -19,8 +19,7 @@
import android.annotation.SuppressLint
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
import androidx.window.core.SpecificationComputer.Companion.startSpecification
import androidx.window.core.VerificationMode
@@ -35,37 +34,77 @@
* - Layout direction — Specifies whether the parent window is split vertically or
* horizontally and in which direction the primary and secondary containers are respectively
* positioned (left to right, right to left, top to bottom, and so forth)
- * - Animation background color — The color of the background during animation of the split
- * involving this `SplitAttributes` object if the animation requires a background
+ * - Animation params — The parameters for the animation of the split involving this
+ * `SplitAttributes` object
+ * - Divider attributes — Specifies whether a divider is needed between the split containers
+ * and the properties of the divider, including the color, the width, whether the divider is
+ * draggable, etc.
*
* Attributes can be configured by:
* - Setting the default `SplitAttributes` using [SplitPairRule.Builder.setDefaultSplitAttributes]
* or [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
- * - Setting `splitRatio`, `splitLayoutDirection`, and `animationBackgroundColor` attributes in
+ * - Setting `splitRatio`, `splitLayoutDirection`, and `animationParams` attributes in
* `<SplitPairRule>` or `<SplitPlaceholderRule>` tags in an XML configuration file. The attributes
- * are parsed as [SplitType], [LayoutDirection], and [BackgroundColor], respectively. Note that
- * [SplitType.HingeSplitType] is not supported XML format.
- * - Set `SplitAttributes` calculation function by [SplitController.setSplitAttributesCalculator] to
- * customize the `SplitAttributes` for a given device and window state.
+ * are parsed as [SplitType], [LayoutDirection], and [EmbeddingAnimationParams], respectively.
+ * Note that [SplitType.HingeSplitType] is not supported XML format.
+ * - Using [SplitAttributesCalculator.computeSplitAttributesForParams] to customize the
+ * `SplitAttributes` for a given device and window state.
*
+ * @property splitType The split type attribute. Defaults to an equal split of the parent window for
+ * the primary and secondary containers.
+ * @property layoutDirection The layout direction of the parent window split. The default is based
+ * on locale value.
+ * @property animationParams The animation params to specify the animation transitions of the split
+ * involving this `SplitAttributes` object. The default is to use the current theme window
+ * background color and system transitions.
+ * @property dividerAttributes The [DividerAttributes] for this split. Defaults to
+ * [DividerAttributes.NO_DIVIDER], which means no divider is requested.
* @see SplitAttributes.SplitType
* @see SplitAttributes.LayoutDirection
+ * @see EmbeddingAnimationParams
*/
class SplitAttributes
-@RestrictTo(LIBRARY_GROUP)
+@JvmOverloads
constructor(
-
- /**
- * The split type attribute. Defaults to an equal split of the parent window for the primary and
- * secondary containers.
- */
val splitType: SplitType = SPLIT_TYPE_EQUAL,
-
- /**
- * The layout direction attribute for the parent window split. The default is based on locale.
- */
val layoutDirection: LayoutDirection = LOCALE,
+ val animationParams: EmbeddingAnimationParams = EmbeddingAnimationParams.Builder().build(),
+ val dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER,
) {
+ @Deprecated(
+ message = "Use default constructor with [animationParams] instead",
+ )
+ constructor(
+ splitType: SplitType,
+ layoutDirection: LayoutDirection,
+ animationBackground: EmbeddingAnimationBackground,
+ dividerAttributes: DividerAttributes,
+ ) : this(
+ splitType,
+ layoutDirection,
+ EmbeddingAnimationParams.Builder().setAnimationBackground(animationBackground).build(),
+ dividerAttributes
+ ) {}
+
+ @Deprecated(
+ message = "Use default constructor with [animationParams] instead",
+ )
+ constructor(
+ splitType: SplitType,
+ layoutDirection: LayoutDirection,
+ animationBackground: EmbeddingAnimationBackground
+ ) : this(
+ splitType,
+ layoutDirection,
+ EmbeddingAnimationParams.Builder().setAnimationBackground(animationBackground).build()
+ ) {}
+
+ @Deprecated(
+ message = "Use [animationParams] instead",
+ replaceWith = ReplaceWith("this.animationParams.animationBackground")
+ )
+ val animationBackground: EmbeddingAnimationBackground
+ get() = this.animationParams.animationBackground
/**
* The type of parent window split, which defines the proportion of the parent window occupied
@@ -333,6 +372,8 @@
override fun hashCode(): Int {
var result = splitType.hashCode()
result = result * 31 + layoutDirection.hashCode()
+ result = result * 31 + animationParams.hashCode()
+ result = result * 31 + dividerAttributes.hashCode()
return result
}
@@ -345,7 +386,10 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SplitAttributes) return false
- return splitType == other.splitType && layoutDirection == other.layoutDirection
+ return splitType == other.splitType &&
+ layoutDirection == other.layoutDirection &&
+ animationParams == other.animationParams &&
+ dividerAttributes == other.dividerAttributes
}
/**
@@ -355,17 +399,23 @@
*/
override fun toString(): String =
"${SplitAttributes::class.java.simpleName}:" +
- "{splitType=$splitType, layoutDir=$layoutDirection }"
+ "{splitType=$splitType, layoutDir=$layoutDirection, " +
+ "animationParams=$animationParams, " +
+ "dividerAttributes=$dividerAttributes }"
/**
* Builder for creating an instance of [SplitAttributes].
* - The default split type is an equal split between primary and secondary containers.
* - The default layout direction is based on locale.
- * - The default animation background color is to use the current theme window background color.
+ * - The default animation params is to use the current theme window background color and system
+ * transitions.
+ * - The default divider attributes is not to use divider.
*/
class Builder {
private var splitType = SPLIT_TYPE_EQUAL
private var layoutDirection = LOCALE
+ private var animationParams = EmbeddingAnimationParams.Builder().build()
+ private var dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER
/**
* Sets the split type attribute.
@@ -391,12 +441,44 @@
this.layoutDirection = layoutDirection
}
+ /** @deprecated Use [setAnimationParams] instead. */
+ @Deprecated(message = "Use [setAnimationParams] instead")
+ @RequiresWindowSdkExtension(5)
+ fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder = apply {
+ this.animationParams =
+ EmbeddingAnimationParams.Builder().setAnimationBackground(background).build()
+ }
+
/**
- * Builds a `SplitAttributes` instance with the attributes specified by [setSplitType] and
- * [setLayoutDirection].
+ * Sets the animation params to use during animation of the split involving this
+ * `SplitAttributes` object if the animation requires a background color or non-default
+ * transitions.
+ *
+ * [EmbeddingAnimationParams] can be supported only if the Window Extensions version of the
+ * target device is equals or higher than required API level. Otherwise, it would be no-op
+ * when setting the [EmbeddingAnimationParams] on a target device that has lower API level.
+ *
+ * @param params the animation params.
+ * @return this `Builder`.
+ */
+ @RequiresWindowSdkExtension(7)
+ fun setAnimationParams(params: EmbeddingAnimationParams): Builder = apply {
+ this.animationParams = params
+ }
+
+ /** Sets the [DividerAttributes]. */
+ @RequiresWindowSdkExtension(6)
+ fun setDividerAttributes(dividerAttributes: DividerAttributes): Builder = apply {
+ this.dividerAttributes = dividerAttributes
+ }
+
+ /**
+ * Builds a `SplitAttributes` instance with the attributes specified by [setSplitType],
+ * [setLayoutDirection], and [setAnimationParams].
*
* @return The new `SplitAttributes` instance.
*/
- fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)
+ fun build(): SplitAttributes =
+ SplitAttributes(splitType, layoutDirection, animationParams, dividerAttributes)
}
}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index fc28d14e..91a498b 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -22,7 +22,6 @@
import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowProperties
import androidx.window.WindowSdkExtensions
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.layout.WindowMetrics
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -80,6 +79,56 @@
get() = embeddingBackend.splitSupportStatus
/**
+ * Pins the top-most [ActivityStack] to keep the stack of the Activities to be always positioned
+ * on top. The rest of the activities in the Task will be split with the pinned [ActivityStack].
+ * The pinned [ActivityStack] would also have isolated activity navigation in which only the
+ * activities that are started from the pinned [ActivityStack] can be added on top of the
+ * [ActivityStack].
+ *
+ * The pinned [ActivityStack] is unpinned whenever the pinned [ActivityStack] is expanded. Use
+ * [SplitPinRule.Builder.setSticky] if the same [ActivityStack] should be pinned again whenever
+ * the [ActivityStack] is on top and split with another [ActivityStack] again.
+ *
+ * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than or
+ * equal to 5.
+ *
+ * @param taskId The id of the Task that top [ActivityStack] should be pinned.
+ * @param splitPinRule The SplitRule that specifies how the top [ActivityStack] should be split
+ * with others.
+ * @return Returns `true` if the top [ActivityStack] is successfully pinned. Otherwise, `false`.
+ * Few examples are:
+ * 1. There's no [ActivityStack].
+ * 2. There is already an existing pinned [ActivityStack].
+ * 3. There's no other [ActivityStack] to split with the top [ActivityStack].
+ *
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 5.
+ */
+ @RequiresWindowSdkExtension(5)
+ fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+ return embeddingBackend.pinTopActivityStack(taskId, splitPinRule)
+ }
+
+ /**
+ * Unpins the pinned [ActivityStack]. The [ActivityStack] will still be the top-most
+ * [ActivityStack] right after unpinned, and the [ActivityStack] could be expanded or continue
+ * to be split with the next top [ActivityStack] if the current state matches any of the
+ * existing [SplitPairRule]. It is a no-op call if the task does not have a pinned
+ * [ActivityStack].
+ *
+ * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than or
+ * equal to 5.
+ *
+ * @param taskId The id of the Task that top [ActivityStack] should be unpinned.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 5.
+ */
+ @RequiresWindowSdkExtension(5)
+ fun unpinTopActivityStack(taskId: Int) {
+ embeddingBackend.unpinTopActivityStack(taskId)
+ }
+
+ /**
* Sets or replaces the previously registered [SplitAttributes] calculator.
*
* **Note** that it's callers' responsibility to check if this API is supported by checking
@@ -141,27 +190,6 @@
}
/**
- * Triggers a [SplitAttributes] update callback for the current topmost and visible split layout
- * if there is one. This method can be used when a change to the split presentation originates
- * from an application state change. Changes that are driven by parent window changes or new
- * activity starts invoke the callback provided in [setSplitAttributesCalculator] automatically
- * without the need to call this function.
- *
- * The top [SplitInfo] is usually the last element of [SplitInfo] list which was received from
- * the callback registered in [splitInfoList].
- *
- * The call will be ignored if there is no visible split.
- *
- * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
- * than 3.
- */
- @ExperimentalWindowApi
- @RequiresWindowSdkExtension(3)
- fun invalidateTopVisibleSplitAttributes() {
- embeddingBackend.invalidateTopVisibleSplitAttributes()
- }
-
- /**
* Updates the [SplitAttributes] of a split pair. This is an alternative to using a split
* attributes calculator callback set in [setSplitAttributesCalculator], useful when apps only
* need to update the splits in a few cases proactively but rely on the default split attributes
@@ -176,15 +204,15 @@
* - A new Activity being launched.
* - A window or device state updates (e,g. due to screen rotation or folding state update).
*
- * In most cases it is suggested to use [invalidateTopVisibleSplitAttributes] if
- * [SplitAttributes] calculator callback is used.
+ * In most cases it is suggested to use
+ * [ActivityEmbeddingController.invalidateTopVisibleActivityStacks] if a calculator has been set
+ * through [setSplitAttributesCalculator].
*
* @param splitInfo the split pair to update
* @param splitAttributes the [SplitAttributes] to be applied
* @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
* than 3.
*/
- @ExperimentalWindowApi
@RequiresWindowSdkExtension(3)
fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
index 0a8b4dc..40fa692 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -19,21 +19,89 @@
import android.app.Activity
import android.os.IBinder
import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.embedding.SplitInfo.Token
/** Describes a split pair of two containers with activities. */
+@Suppress("Deprecation") // To compat with device with version 3 and 4.
class SplitInfo
-@RestrictTo(LIBRARY_GROUP)
-constructor(
+private constructor(
/** The [ActivityStack] representing the primary split container. */
val primaryActivityStack: ActivityStack,
/** The [ActivityStack] representing the secondary split container. */
val secondaryActivityStack: ActivityStack,
/** The [SplitAttributes] of this split pair. */
val splitAttributes: SplitAttributes,
+ @Deprecated(
+ message = "Use [token] instead",
+ replaceWith =
+ ReplaceWith(
+ expression = "SplitInfo.token",
+ imports = arrayOf("androidx.window.embedding.SplitInfo"),
+ )
+ )
+ private val binder: IBinder?,
/** A token uniquely identifying this `SplitInfo`. */
- internal val token: IBinder,
+ private val token: Token?,
) {
+ @RequiresWindowSdkExtension(5)
+ internal constructor(
+ primaryActivityStack: ActivityStack,
+ secondaryActivityStack: ActivityStack,
+ splitAttributes: SplitAttributes,
+ token: Token,
+ ) : this(primaryActivityStack, secondaryActivityStack, splitAttributes, binder = null, token)
+
+ /** Creates SplitInfo for [WindowSdkExtensions.extensionVersion] 3 and 4. */
+ @RequiresWindowSdkExtension(3)
+ internal constructor(
+ primaryActivityStack: ActivityStack,
+ secondaryActivityStack: ActivityStack,
+ splitAttributes: SplitAttributes,
+ binder: IBinder,
+ ) : this(
+ primaryActivityStack,
+ secondaryActivityStack,
+ splitAttributes,
+ binder,
+ token = null,
+ ) {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
+ }
+
+ /**
+ * Creates SplitInfo ONLY for testing.
+ *
+ * @param primaryActivityStack the [ActivityStack] representing the primary split container.
+ * @param secondaryActivityStack the [ActivityStack] representing the secondary split container.
+ * @param splitAttributes the [SplitAttributes] of this split pair.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ constructor(
+ primaryActivityStack: ActivityStack,
+ secondaryActivityStack: ActivityStack,
+ splitAttributes: SplitAttributes,
+ ) : this(
+ primaryActivityStack,
+ secondaryActivityStack,
+ splitAttributes,
+ binder = null,
+ token = null,
+ )
+
+ @RequiresWindowSdkExtension(3)
+ internal fun getBinder(): IBinder = let {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
+ requireNotNull(binder)
+ }
+
+ @RequiresWindowSdkExtension(5)
+ internal fun getToken(): Token = let {
+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+ requireNotNull(token)
+ }
+
/**
* Whether the [primaryActivityStack] or the [secondaryActivityStack] in this [SplitInfo]
* contains the [activity].
@@ -50,6 +118,7 @@
if (secondaryActivityStack != other.secondaryActivityStack) return false
if (splitAttributes != other.splitAttributes) return false
if (token != other.token) return false
+ if (binder != other.binder) return false
return true
}
@@ -59,6 +128,7 @@
result = 31 * result + secondaryActivityStack.hashCode()
result = 31 * result + splitAttributes.hashCode()
result = 31 * result + token.hashCode()
+ result = 31 * result + binder.hashCode()
return result
}
@@ -68,7 +138,12 @@
append("primaryActivityStack=$primaryActivityStack, ")
append("secondaryActivityStack=$secondaryActivityStack, ")
append("splitAttributes=$splitAttributes, ")
- append("token=$token")
+ if (token != null) {
+ append("token=$token")
+ }
+ if (binder != null) {
+ append("binder=$binder")
+ }
append("}")
}
}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
new file mode 100644
index 0000000..73c45cf
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2023 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT
+
+/**
+ * Split configuration rules for pinning an [ActivityStack]. Define how the pinned [ActivityStack]
+ * should be displayed side-by-side with the other [ActivityStack].
+ */
+class SplitPinRule
+internal constructor(
+ /** A unique string to identify this [SplitPinRule]. */
+ tag: String? = null,
+ /** The default [SplitAttributes] to apply on the pin container and the paired container. */
+ defaultSplitAttributes: SplitAttributes,
+ /**
+ * Whether this rule should be sticky. If the value is `false`, this rule be removed whenever
+ * the pinned [ActivityStack] is unpinned. Set to `true` if the rule should be applied whenever
+ * once again possible (e.g. the host Task bounds satisfies the size and aspect ratio
+ * requirements).
+ */
+ val isSticky: Boolean,
+ @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+ @IntRange(from = 0) minHeightDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+ @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+ maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
+ maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+) :
+ SplitRule(
+ tag,
+ minWidthDp,
+ minHeightDp,
+ minSmallestWidthDp,
+ maxAspectRatioInPortrait,
+ maxAspectRatioInLandscape,
+ defaultSplitAttributes
+ ) {
+
+ /** Builder for [SplitPinRule]. */
+ class Builder {
+ private var tag: String? = null
+ @IntRange(from = 0) private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+ @IntRange(from = 0) private var minHeightDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+ @IntRange(from = 0) private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+ private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+ private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+ private var defaultSplitAttributes = SplitAttributes.Builder().build()
+ private var isSticky: Boolean = false
+
+ /**
+ * Sets the smallest value of width of the parent window when the split should be used, in
+ * DP. When the window size is smaller than requested here, activities in the secondary
+ * container will be stacked on top of the activities in the primary one, completely
+ * overlapping them.
+ *
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+ *
+ * @param minWidthDp the smallest value of width of the parent window when the split should
+ * be used, in DP.
+ */
+ fun setMinWidthDp(@IntRange(from = 0) minWidthDp: Int): Builder = apply {
+ this.minWidthDp = minWidthDp
+ }
+
+ /**
+ * Sets the smallest value of height of the parent task window when the split should be
+ * used, in DP. When the window size is smaller than requested here, activities in the
+ * secondary container will be stacked on top of the activities in the primary one,
+ * completely overlapping them.
+ *
+ * It is useful if it's necessary to split the parent window horizontally for this
+ * [SplitPinRule].
+ *
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+ *
+ * @param minHeightDp the smallest value of height of the parent task window when the split
+ * should be used, in DP.
+ * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+ * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+ */
+ fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder = apply {
+ this.minHeightDp = minHeightDp
+ }
+
+ /**
+ * Sets the smallest value of the smallest possible width of the parent window in any
+ * rotation when the split should be used, in DP. When the window size is smaller than
+ * requested here, activities in the secondary container will be stacked on top of the
+ * activities in the primary one, completely overlapping them.
+ *
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+ *
+ * @param minSmallestWidthDp the smallest value of the smallest possible width of the parent
+ * window in any rotation when the split should be used, in DP.
+ */
+ fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder = apply {
+ this.minSmallestWidthDp = minSmallestWidthDp
+ }
+
+ /**
+ * Sets the largest value of the aspect ratio, expressed as `height / width` in decimal
+ * form, of the parent window bounds in portrait when the split should be used. When the
+ * window aspect ratio is greater than requested here, activities in the secondary container
+ * will be stacked on top of the activities in the primary one, completely overlapping them.
+ *
+ * This value is only used when the parent window is in portrait (height >= width).
+ *
+ * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is
+ * the recommend value to only allow split when the parent window is not too stretched in
+ * portrait.
+ *
+ * @param aspectRatio the largest value of the aspect ratio, expressed as `height / width`
+ * in decimal form, of the parent window bounds in portrait when the split should be used.
+ * @see EmbeddingAspectRatio.ratio
+ * @see EmbeddingAspectRatio.ALWAYS_ALLOW
+ * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
+ */
+ fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder = apply {
+ this.maxAspectRatioInPortrait = aspectRatio
+ }
+
+ /**
+ * Sets the largest value of the aspect ratio, expressed as `width / height` in decimal
+ * form, of the parent window bounds in landscape when the split should be used. When the
+ * window aspect ratio is greater than requested here, activities in the secondary container
+ * will be stacked on top of the activities in the primary one, completely overlapping them.
+ *
+ * This value is only used when the parent window is in landscape (width > height).
+ *
+ * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which
+ * is the recommend value to always allow split when the parent window is in landscape.
+ *
+ * @param aspectRatio the largest value of the aspect ratio, expressed as `width / height`
+ * in decimal form, of the parent window bounds in landscape when the split should be
+ * used.
+ * @see EmbeddingAspectRatio.ratio
+ * @see EmbeddingAspectRatio.ALWAYS_ALLOW
+ * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
+ */
+ fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder = apply {
+ this.maxAspectRatioInLandscape = aspectRatio
+ }
+
+ /**
+ * Sets the default [SplitAttributes] to apply on the activity containers pair when the host
+ * task bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp],
+ * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.
+ *
+ * @param defaultSplitAttributes the default [SplitAttributes] to apply on the activity
+ * containers pair when the host task bounds satisfy all the rule requirements.
+ */
+ fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder = apply {
+ this.defaultSplitAttributes = defaultSplitAttributes
+ }
+
+ /**
+ * Sets a unique string to identify this [SplitPinRule], which defaults to `null`. The
+ * suggested usage is to set the tag to be able to differentiate between different rules in
+ * the [SplitAttributesCalculatorParams.splitRuleTag].
+ *
+ * @param tag unique string to identify this [SplitPinRule].
+ */
+ fun setTag(tag: String?): Builder = apply { this.tag = tag }
+
+ /**
+ * Sets this rule to be sticky.
+ *
+ * @param isSticky whether to be a sticky rule.
+ * @see isSticky
+ */
+ fun setSticky(isSticky: Boolean): Builder = apply { this.isSticky = isSticky }
+
+ /**
+ * Builds a [SplitPinRule] instance.
+ *
+ * @return The new [SplitPinRule] instance.
+ */
+ fun build() =
+ SplitPinRule(
+ tag,
+ defaultSplitAttributes,
+ isSticky,
+ minWidthDp,
+ minHeightDp,
+ minSmallestWidthDp,
+ maxAspectRatioInPortrait,
+ maxAspectRatioInLandscape
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SplitPinRule) return false
+
+ if (!super.equals(other)) return false
+ if (isSticky != other.isSticky) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = 31 * result + isSticky.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "${SplitPinRule::class.java.simpleName}{" +
+ "tag=$tag" +
+ ", defaultSplitAttributes=$defaultSplitAttributes" +
+ ", isSticky=$isSticky" +
+ "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index adc8732..393862c 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -129,6 +129,8 @@
* The default [SplitAttributes] to apply on the activity containers pair when the host task
* bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp], [maxAspectRatioInPortrait]
* and [maxAspectRatioInLandscape] requirements.
+ *
+ * It is set to split the host parent task vertically and equally by default.
*/
val defaultSplitAttributes: SplitAttributes,
) : EmbeddingRule(tag) {
diff --git a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
index 5ad6e24..75f172d 100644
--- a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
@@ -30,10 +30,13 @@
import androidx.window.reflection.ReflectionUtils.doesReturn
import androidx.window.reflection.ReflectionUtils.isPublic
import androidx.window.reflection.ReflectionUtils.validateReflection
+import androidx.window.reflection.WindowExtensionsConstants.DISPLAY_FOLD_FEATURE_CLASS
import androidx.window.reflection.WindowExtensionsConstants.FOLDING_FEATURE_CLASS
import androidx.window.reflection.WindowExtensionsConstants.JAVA_CONSUMER
+import androidx.window.reflection.WindowExtensionsConstants.SUPPORTED_WINDOW_FEATURES_CLASS
import androidx.window.reflection.WindowExtensionsConstants.WINDOW_CONSUMER
import androidx.window.reflection.WindowExtensionsConstants.WINDOW_LAYOUT_COMPONENT_CLASS
+import java.lang.reflect.ParameterizedType
/**
* Reflection Guard for [WindowLayoutComponent]. This will go through the [WindowLayoutComponent]'s
@@ -63,13 +66,12 @@
if (!isWindowLayoutComponentAccessible()) {
return false
}
- // TODO(b/267831038): can fallback to VendorApiLevel1 when level2 is not match
- // but level 1 is matched
- return when (ExtensionsUtil.safeVendorApiLevel) {
- 1 -> hasValidVendorApiLevel1()
- in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
- // TODO(b/267956499): add hasValidVendorApiLevel3
- else -> false
+ val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
+ return when {
+ vendorApiLevel < 1 -> false
+ vendorApiLevel == 1 -> hasValidVendorApiLevel1()
+ vendorApiLevel < 5 -> hasValidVendorApiLevel2()
+ else -> hasValidVendorApiLevel6()
}
}
@@ -100,6 +102,14 @@
return hasValidVendorApiLevel1() && isMethodWindowLayoutInfoListenerWindowConsumerValid()
}
+ @VisibleForTesting
+ internal fun hasValidVendorApiLevel6(): Boolean {
+ return hasValidVendorApiLevel2() &&
+ isDisplayFoldFeatureValid() &&
+ isSupportedWindowFeaturesValid() &&
+ isGetSupportedWindowFeaturesValid()
+ }
+
private fun isWindowLayoutProviderValid(): Boolean {
return validateReflection("WindowExtensions#getWindowLayoutComponent is not valid") {
val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
@@ -167,6 +177,63 @@
}
}
+ private fun isDisplayFoldFeatureValid(): Boolean {
+ return validateReflection("DisplayFoldFeature is not valid") {
+ val displayFoldFeatureClass = displayFoldFeatureClass
+
+ val getTypeMethod = displayFoldFeatureClass.getMethod("getType")
+ val hasPropertyMethod =
+ displayFoldFeatureClass.getMethod("hasProperty", Int::class.java)
+ val hasPropertiesMethod =
+ displayFoldFeatureClass.getMethod("hasProperties", IntArray::class.java)
+
+ getTypeMethod.isPublic &&
+ getTypeMethod.doesReturn(Int::class.java) &&
+ hasPropertyMethod.isPublic &&
+ hasPropertyMethod.doesReturn(Boolean::class.java) &&
+ hasPropertiesMethod.isPublic &&
+ hasPropertiesMethod.doesReturn(Boolean::class.java)
+ }
+ }
+
+ private fun isSupportedWindowFeaturesValid(): Boolean {
+ return validateReflection("SupportedWindowFeatures is not valid") {
+ val supportedWindowFeaturesClass = supportedWindowFeaturesClass
+
+ val getDisplayFoldFeaturesMethod =
+ supportedWindowFeaturesClass.getMethod("getDisplayFoldFeatures")
+ val returnTypeGeneric =
+ (getDisplayFoldFeaturesMethod.genericReturnType as ParameterizedType)
+ .actualTypeArguments[0]
+ as Class<*>
+
+ getDisplayFoldFeaturesMethod.isPublic &&
+ getDisplayFoldFeaturesMethod.doesReturn(List::class.java) &&
+ returnTypeGeneric == displayFoldFeatureClass
+ }
+ }
+
+ private fun isGetSupportedWindowFeaturesValid(): Boolean {
+ return validateReflection("WindowLayoutComponent#getSupportedWindowFeatures is not valid") {
+ val windowLayoutComponent = windowLayoutComponentClass
+ val getSupportedWindowFeaturesMethod =
+ windowLayoutComponent.getMethod("getSupportedWindowFeatures")
+
+ getSupportedWindowFeaturesMethod.isPublic &&
+ getSupportedWindowFeaturesMethod.doesReturn(supportedWindowFeaturesClass)
+ }
+ }
+
+ private val displayFoldFeatureClass: Class<*>
+ get() {
+ return loader.loadClass(DISPLAY_FOLD_FEATURE_CLASS)
+ }
+
+ private val supportedWindowFeaturesClass: Class<*>
+ get() {
+ return loader.loadClass(SUPPORTED_WINDOW_FEATURES_CLASS)
+ }
+
private val foldingFeatureClass: Class<*>
get() {
return loader.loadClass(FOLDING_FEATURE_CLASS)
diff --git a/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
new file mode 100644
index 0000000..0e29629
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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.layout
+
+/** A class to represent a posture that the device supports. */
+class SupportedPosture internal constructor(private val rawValue: Int) {
+
+ override fun toString(): String {
+ return when (this) {
+ TABLETOP -> "TABLETOP"
+ else -> "UNKNOWN"
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null) return false
+ if (other::class != SupportedPosture::class) return false
+
+ other as SupportedPosture
+
+ return rawValue == other.rawValue
+ }
+
+ override fun hashCode(): Int {
+ return rawValue
+ }
+
+ companion object {
+ /** The posture where there is a single fold in the half-opened state. */
+ @JvmField val TABLETOP: SupportedPosture = SupportedPosture(0)
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
index ffb3a1f..d4b7534 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -23,6 +23,7 @@
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.annotation.UiContext
+import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
import androidx.window.core.ConsumerAdapter
import androidx.window.layout.adapter.WindowBackend
@@ -96,6 +97,23 @@
*/
fun windowLayoutInfo(activity: Activity): Flow<WindowLayoutInfo>
+ /**
+ * Returns the [List] of [SupportedPosture] values. This value will not change during runtime.
+ * These values are for determining if the device supports the given [SupportedPosture] but does
+ * not mean the device is in the given [SupportedPosture]. Use [windowLayoutInfo] to determine
+ * the current state of the [DisplayFeature]'s on the device.
+ *
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+ * than 6.
+ * @throws NotImplementedError if a derived test class does not override this method.
+ * @see windowLayoutInfo
+ */
+ @RequiresWindowSdkExtension(version = 6)
+ val supportedPostures: List<SupportedPosture>
+ get() {
+ throw NotImplementedError("Method was not implemented.")
+ }
+
companion object {
private val DEBUG = false
@@ -134,7 +152,12 @@
@JvmStatic
fun getOrCreate(context: Context): WindowInfoTracker {
val backend = extensionBackend ?: SidecarWindowBackend.getInstance(context)
- val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, backend)
+ val repo =
+ WindowInfoTrackerImpl(
+ WindowMetricsCalculatorCompat(),
+ backend,
+ WindowSdkExtensions.getInstance()
+ )
return decorator.decorate(repo)
}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
index e6702d0..308042f 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
@@ -20,6 +20,7 @@
import android.content.Context
import androidx.annotation.UiContext
import androidx.core.util.Consumer
+import androidx.window.WindowSdkExtensions
import androidx.window.layout.adapter.WindowBackend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@@ -36,7 +37,8 @@
*/
internal class WindowInfoTrackerImpl(
private val windowMetricsCalculator: WindowMetricsCalculator,
- private val windowBackend: WindowBackend
+ private val windowBackend: WindowBackend,
+ private val windowSdkExtensions: WindowSdkExtensions
) : WindowInfoTracker {
/**
@@ -61,4 +63,10 @@
}
.flowOn(Dispatchers.Main)
}
+
+ override val supportedPostures: List<SupportedPosture>
+ get() {
+ windowSdkExtensions.requireExtensionVersion(6)
+ return windowBackend.supportedPostures
+ }
}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
index 7aaa8e1..6707353 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
@@ -17,6 +17,7 @@
import android.graphics.Rect
import android.os.Build.VERSION_CODES
+import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.core.view.WindowInsetsCompat
@@ -34,15 +35,23 @@
class WindowMetrics
internal constructor(
private val _bounds: Bounds,
- private val _windowInsetsCompat: WindowInsetsCompat
+ private val _windowInsetsCompat: WindowInsetsCompat,
+
+ /**
+ * Returns the logical density of the display this window is in.
+ *
+ * @see [DisplayMetrics.density]
+ */
+ val density: Float
) {
/** An internal constructor for [WindowMetrics] */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
constructor(
bounds: Rect,
- insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build()
- ) : this(Bounds(bounds), insets)
+ insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build(),
+ density: Float
+ ) : this(Bounds(bounds), insets, density)
/**
* Returns a new [Rect] describing the bounds of the area the window occupies.
@@ -68,6 +77,7 @@
if (_bounds != other._bounds) return false
if (_windowInsetsCompat != other._windowInsetsCompat) return false
+ if (density != other.density) return false
return true
}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
index 9630be0..04060a1 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
@@ -28,6 +28,7 @@
import androidx.annotation.UiContext
import androidx.core.view.WindowInsetsCompat
import androidx.window.core.Bounds
+import androidx.window.layout.util.WindowMetricsCompatHelper
/** An interface to calculate the [WindowMetrics] for an [Activity] or a [UiContext]. */
interface WindowMetricsCalculator {
@@ -123,10 +124,11 @@
companion object {
private var decorator: (WindowMetricsCalculator) -> WindowMetricsCalculator = { it }
+ private val windowMetricsCalculatorCompat = WindowMetricsCalculatorCompat()
@JvmStatic
fun getOrCreate(): WindowMetricsCalculator {
- return decorator(WindowMetricsCalculatorCompat)
+ return decorator(windowMetricsCalculatorCompat)
}
@JvmStatic
@@ -145,18 +147,20 @@
* Converts [Android API WindowMetrics][AndroidWindowMetrics] to
* [Jetpack version WindowMetrics][WindowMetrics]
*/
- @Suppress("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.R)
- internal fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics): WindowMetrics =
- WindowMetrics(
- windowMetrics.bounds,
- WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
- )
+ internal fun translateWindowMetrics(
+ windowMetrics: AndroidWindowMetrics,
+ density: Float
+ ): WindowMetrics {
+ return WindowMetricsCompatHelper.getInstance()
+ .translateWindowMetrics(windowMetrics, density)
+ }
internal fun fromDisplayMetrics(displayMetrics: DisplayMetrics): WindowMetrics {
return WindowMetrics(
Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
- WindowInsetsCompat.Builder().build()
+ WindowInsetsCompat.Builder().build(),
+ displayMetrics.density
)
}
}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
index fd77478..ffaa59a 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
@@ -15,40 +15,18 @@
*/
package androidx.window.layout
-import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Point
-import android.graphics.Rect
import android.inputmethodservice.InputMethodService
-import android.os.Build
-import android.os.Build.VERSION_CODES
-import android.util.Log
-import android.view.Display
-import android.view.DisplayCutout
-import android.view.WindowManager
-import androidx.annotation.RequiresApi
import androidx.annotation.UiContext
-import androidx.annotation.VisibleForTesting
import androidx.core.view.WindowInsetsCompat
-import androidx.window.core.Bounds
-import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
-import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowBounds
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowMetrics
-import androidx.window.layout.util.ContextCompatHelperApi30.maximumWindowBounds
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
-import java.lang.reflect.InvocationTargetException
+import androidx.window.layout.util.DensityCompatHelper
+import androidx.window.layout.util.WindowMetricsCompatHelper
/** Helper class used to compute window metrics across Android versions. */
-internal object WindowMetricsCalculatorCompat : WindowMetricsCalculator {
-
- private val TAG: String = WindowMetricsCalculatorCompat::class.java.simpleName
+internal class WindowMetricsCalculatorCompat(
+ private val densityCompatHelper: DensityCompatHelper = DensityCompatHelper.getInstance()
+) : WindowMetricsCalculator {
/**
* Computes the current [WindowMetrics] for a given [Context]. The context can be either an
@@ -57,33 +35,8 @@
* @see WindowMetricsCalculator.computeCurrentWindowMetrics
*/
override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
- // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
- return currentWindowMetrics(context)
- } else {
- when (val unwrappedContext = unwrapUiContext(context)) {
- is Activity -> {
- return computeCurrentWindowMetrics(unwrappedContext)
- }
- is InputMethodService -> {
- val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
- // On older SDK levels, the app and IME could show up on different displays.
- // However, there isn't a way for us to figure this out from the application
- // layer. But, this should be good enough for now given the small likelihood of
- // IMEs showing up on non-primary displays on these SDK levels.
- @Suppress("DEPRECATION")
- val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
-
- // IME occupies the whole display bounds.
- val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
- return WindowMetrics(imeBounds)
- }
- else -> {
- throw IllegalArgumentException("$context is not a UiContext")
- }
- }
- }
+ return WindowMetricsCompatHelper.getInstance()
+ .currentWindowMetrics(context, densityCompatHelper)
}
/**
@@ -92,26 +45,8 @@
* @see WindowMetricsCalculator.computeCurrentWindowMetrics
*/
override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
- val bounds =
- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
- currentWindowBounds(activity)
- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
- computeWindowBoundsQ(activity)
- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
- computeWindowBoundsP(activity)
- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
- computeWindowBoundsN(activity)
- } else {
- computeWindowBoundsIceCreamSandwich(activity)
- }
- // TODO (b/233899790): compute insets for other platform versions below R
- val windowInsetsCompat =
- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
- computeWindowInsetsCompat(activity)
- } else {
- WindowInsetsCompat.Builder().build()
- }
- return WindowMetrics(Bounds(bounds), windowInsetsCompat)
+ return WindowMetricsCompatHelper.getInstance()
+ .currentWindowMetrics(activity, densityCompatHelper)
}
/**
@@ -120,7 +55,8 @@
* @see WindowMetricsCalculator.computeMaximumWindowMetrics
*/
override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
- return computeMaximumWindowMetrics(activity as Context)
+ return WindowMetricsCompatHelper.getInstance()
+ .maximumWindowMetrics(activity, densityCompatHelper)
}
/**
@@ -129,288 +65,8 @@
* @See WindowMetricsCalculator.computeMaximumWindowMetrics
*/
override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
- // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
- val bounds =
- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
- maximumWindowBounds(context)
- } else {
- val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- // [WindowManager#getDefaultDisplay] is deprecated but we have this for
- // compatibility with older versions, as we can't reliably get the display
- // associated
- // with a Context through public APIs either.
- @Suppress("DEPRECATION") val display = wm.defaultDisplay
- val displaySize = getRealSizeForDisplay(display)
- Rect(0, 0, displaySize.x, displaySize.y)
- }
- // TODO (b/233899790): compute insets for other platform versions below R
- val windowInsetsCompat =
- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
- computeWindowInsetsCompat(context)
- } else {
- WindowInsetsCompat.Builder().build()
- }
- return WindowMetrics(Bounds(bounds), windowInsetsCompat)
- }
-
- /** Computes the window bounds for [Build.VERSION_CODES.Q]. */
- @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
- @RequiresApi(VERSION_CODES.Q)
- internal fun computeWindowBoundsQ(activity: Activity): Rect {
- var bounds: Rect
- val config = activity.resources.configuration
- try {
- val windowConfigField =
- Configuration::class.java.getDeclaredField("windowConfiguration")
- windowConfigField.isAccessible = true
- val windowConfig = windowConfigField[config]
- val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
- bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
- } catch (e: NoSuchFieldException) {
- Log.w(TAG, e)
- // If reflection fails for some reason default to the P implementation which still
- // has the ability to account for display cutouts.
- bounds = computeWindowBoundsP(activity)
- } catch (e: NoSuchMethodException) {
- Log.w(TAG, e)
- bounds = computeWindowBoundsP(activity)
- } catch (e: IllegalAccessException) {
- Log.w(TAG, e)
- bounds = computeWindowBoundsP(activity)
- } catch (e: InvocationTargetException) {
- Log.w(TAG, e)
- bounds = computeWindowBoundsP(activity)
- }
- return bounds
- }
-
- /**
- * Computes the window bounds for [Build.VERSION_CODES.P].
- *
- * NOTE: This method may result in incorrect values if the [android.content.res.Resources] value
- * stored at 'navigation_bar_height' does not match the true navigation bar inset on the window.
- */
- @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
- @RequiresApi(VERSION_CODES.P)
- internal fun computeWindowBoundsP(activity: Activity): Rect {
- val bounds = Rect()
- val config = activity.resources.configuration
- try {
- val windowConfigField =
- Configuration::class.java.getDeclaredField("windowConfiguration")
- windowConfigField.isAccessible = true
- val windowConfig = windowConfigField[config]
-
- // In multi-window mode we'll use the WindowConfiguration#mBounds property which
- // should match the window size. Otherwise we'll use the mAppBounds property and
- // will adjust it below.
- if (isInMultiWindowMode(activity)) {
- val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
- bounds.set((getAppBounds.invoke(windowConfig) as Rect))
- } else {
- val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
- bounds.set((getAppBounds.invoke(windowConfig) as Rect))
- }
- } catch (e: NoSuchFieldException) {
- Log.w(TAG, e)
- getRectSizeFromDisplay(activity, bounds)
- } catch (e: NoSuchMethodException) {
- Log.w(TAG, e)
- getRectSizeFromDisplay(activity, bounds)
- } catch (e: IllegalAccessException) {
- Log.w(TAG, e)
- getRectSizeFromDisplay(activity, bounds)
- } catch (e: InvocationTargetException) {
- Log.w(TAG, e)
- getRectSizeFromDisplay(activity, bounds)
- }
- val platformWindowManager = activity.windowManager
-
- // [WindowManager#getDefaultDisplay] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") val currentDisplay = platformWindowManager.defaultDisplay
- val realDisplaySize = Point()
- @Suppress("DEPRECATION") currentDisplay.getRealSize(realDisplaySize)
-
- if (!isInMultiWindowMode(activity)) {
- // The activity is not in multi-window mode. Check if the addition of the
- // navigation bar size to mAppBounds results in the real display size and if so
- // assume the nav bar height should be added to the result.
- val navigationBarHeight = getNavigationBarHeight(activity)
- if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
- bounds.bottom += navigationBarHeight
- } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
- bounds.right += navigationBarHeight
- } else if (bounds.left == navigationBarHeight) {
- bounds.left = 0
- }
- }
- if (
- (bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
- !isInMultiWindowMode(activity)
- ) {
- // If the corrected bounds are not the same as the display size and the activity is
- // not in multi-window mode it is possible there are unreported cutouts inset-ing
- // the window depending on the layoutInCutoutMode. Check for them here by getting
- // the cutout from the display itself.
- val displayCutout = getCutoutForDisplay(currentDisplay)
- if (displayCutout != null) {
- if (bounds.left == safeInsetLeft(displayCutout)) {
- bounds.left = 0
- }
- if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
- bounds.right += safeInsetRight(displayCutout)
- }
- if (bounds.top == safeInsetTop(displayCutout)) {
- bounds.top = 0
- }
- if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
- bounds.bottom += safeInsetBottom(displayCutout)
- }
- }
- }
- return bounds
- }
-
- private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
- // [WindowManager#getDefaultDisplay] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
- // [Display#getRectSize] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
- }
-
- /**
- * Computes the window bounds for platforms between [Build.VERSION_CODES.N] and
- * [Build.VERSION_CODES.O_MR1], inclusive.
- *
- * NOTE: This method may result in incorrect values under the following conditions:
- * * If the activity is in multi-window mode the origin of the returned bounds will always be
- * anchored at (0, 0).
- * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does not
- * match the true navigation bar size the returned bounds will not take into account the
- * navigation bar.
- */
- @RequiresApi(VERSION_CODES.N)
- internal fun computeWindowBoundsN(activity: Activity): Rect {
- val bounds = Rect()
- // [WindowManager#getDefaultDisplay] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
- // [Display#getRectSize] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
- if (!isInMultiWindowMode(activity)) {
- // The activity is not in multi-window mode. Check if the addition of the
- // navigation bar size to Display#getSize() results in the real display size and
- // if so return this value. If not, return the result of Display#getSize().
- val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
- val navigationBarHeight = getNavigationBarHeight(activity)
- if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
- bounds.bottom += navigationBarHeight
- } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
- bounds.right += navigationBarHeight
- }
- }
- return bounds
- }
-
- /**
- * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN] and
- * [Build.VERSION_CODES.M], inclusive.
- *
- * Given that multi-window mode isn't supported before N we simply return the real display size
- * which should match the window size of a full-screen app.
- */
- internal fun computeWindowBoundsIceCreamSandwich(activity: Activity): Rect {
- // [WindowManager#getDefaultDisplay] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
- val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
- val bounds = Rect()
- if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
- // [Display#getRectSize] is deprecated but we have this for
- // compatibility with older versions
- @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
- } else {
- bounds.right = realDisplaySize.x
- bounds.bottom = realDisplaySize.y
- }
- return bounds
- }
-
- /**
- * Returns the full (real) size of the display, in pixels, without subtracting any window decor
- * or applying any compatibility scale factors.
- *
- * The size is adjusted based on the current rotation of the display.
- *
- * @return a point representing the real display size in pixels.
- * @see Display.getRealSize
- */
- @VisibleForTesting
- @Suppress("DEPRECATION")
- internal fun getRealSizeForDisplay(display: Display): Point {
- val size = Point()
- display.getRealSize(size)
- return size
- }
-
- /**
- * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
- *
- * Note: This is error-prone and is **not** the recommended way to determine the size of the
- * overlapping region between the navigation bar and a given window. The best approach is to
- * acquire the [android.view.WindowInsets].
- */
- private fun getNavigationBarHeight(context: Context): Int {
- val resources = context.resources
- val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
- return if (resourceId > 0) {
- resources.getDimensionPixelSize(resourceId)
- } else 0
- }
-
- /**
- * Returns the [DisplayCutout] for the given display. Note that display cutout returned here is
- * for the display and the insets provided are in the display coordinate system.
- *
- * @return the display cutout for the given display.
- */
- @SuppressLint("BanUncheckedReflection")
- @RequiresApi(VERSION_CODES.P)
- private fun getCutoutForDisplay(display: Display): DisplayCutout? {
- var displayCutout: DisplayCutout? = null
- try {
- val displayInfoClass = Class.forName("android.view.DisplayInfo")
- val displayInfoConstructor = displayInfoClass.getConstructor()
- displayInfoConstructor.isAccessible = true
- val displayInfo = displayInfoConstructor.newInstance()
- val getDisplayInfoMethod =
- display.javaClass.getDeclaredMethod("getDisplayInfo", displayInfo.javaClass)
- getDisplayInfoMethod.isAccessible = true
- getDisplayInfoMethod.invoke(display, displayInfo)
- val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
- displayCutoutField.isAccessible = true
- val cutout = displayCutoutField[displayInfo]
- if (cutout is DisplayCutout) {
- displayCutout = cutout
- }
- } catch (e: ClassNotFoundException) {
- Log.w(TAG, e)
- } catch (e: NoSuchMethodException) {
- Log.w(TAG, e)
- } catch (e: NoSuchFieldException) {
- Log.w(TAG, e)
- } catch (e: IllegalAccessException) {
- Log.w(TAG, e)
- } catch (e: InvocationTargetException) {
- Log.w(TAG, e)
- } catch (e: InstantiationException) {
- Log.w(TAG, e)
- }
- return displayCutout
+ return WindowMetricsCompatHelper.getInstance()
+ .maximumWindowMetrics(context, densityCompatHelper)
}
/** [ArrayList] that defines different types of sources causing window insets. */
@@ -425,17 +81,4 @@
WindowInsetsCompat.Type.tappableElement(),
WindowInsetsCompat.Type.displayCutout()
)
-
- /** Computes the current [WindowInsetsCompat] for a given [Context]. */
- @RequiresApi(VERSION_CODES.R)
- internal fun computeWindowInsetsCompat(@UiContext context: Context): WindowInsetsCompat {
- val build = Build.VERSION.SDK_INT
- val windowInsetsCompat =
- if (build >= VERSION_CODES.R) {
- currentWindowInsets(context)
- } else {
- throw Exception("Incompatible SDK version")
- }
- return windowInsetsCompat
- }
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
index 9b7e977..c147914 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
@@ -20,6 +20,8 @@
import androidx.annotation.RestrictTo
import androidx.annotation.UiContext
import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import java.util.concurrent.Executor
@@ -48,4 +50,11 @@
fun hasRegisteredListeners(): Boolean {
return false
}
+
+ /**
+ * Returns a [List] of [SupportedPosture] for the device.
+ *
+ * @throws UnsupportedOperationException if the Window SDK version is less than 6.
+ */
+ @RequiresWindowSdkExtension(version = 6) val supportedPostures: List<SupportedPosture>
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
index 9179308..9d53488 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
@@ -42,7 +42,8 @@
fun newInstance(component: WindowLayoutComponent, adapter: ConsumerAdapter): WindowBackend {
val safeVendorApiLevel = ExtensionsUtil.safeVendorApiLevel
return when {
- safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component)
+ safeVendorApiLevel >= 6 -> ExtensionWindowBackendApi6(component, adapter)
+ safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component, adapter)
safeVendorApiLevel == 1 -> ExtensionWindowBackendApi1(component, adapter)
else -> ExtensionWindowBackendApi0()
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
index 39e2cfe..72b4c87 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
@@ -18,6 +18,7 @@
import android.content.Context
import androidx.core.util.Consumer
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.adapter.WindowBackend
import java.util.concurrent.Executor
@@ -35,4 +36,10 @@
override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
// empty implementation since there are no consumers
}
+
+ override val supportedPostures: List<SupportedPosture>
+ get() =
+ throw UnsupportedOperationException(
+ "supportedPostures is only supported on Window SDK 6."
+ )
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
index a32b465..33c53a7 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
@@ -25,14 +25,15 @@
import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.layout.WindowLayoutComponent
import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.adapter.WindowBackend
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
-internal class ExtensionWindowBackendApi1(
- private val component: WindowLayoutComponent,
+internal open class ExtensionWindowBackendApi1(
+ val component: WindowLayoutComponent,
private val consumerAdapter: ConsumerAdapter
) : WindowBackend {
@@ -125,4 +126,7 @@
listenerToContext.isEmpty() &&
consumerToToken.isEmpty())
}
+
+ override val supportedPostures: List<SupportedPosture>
+ get() = throw UnsupportedOperationException("Extensions version must be at least 6")
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
index 07e5917..0a20ba9 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
@@ -22,15 +22,17 @@
import androidx.annotation.UiContext
import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
+import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.layout.WindowLayoutComponent
import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.adapter.WindowBackend
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
-internal class ExtensionWindowBackendApi2(private val component: WindowLayoutComponent) :
- WindowBackend {
+internal open class ExtensionWindowBackendApi2(
+ component: WindowLayoutComponent,
+ adapter: ConsumerAdapter
+) : ExtensionWindowBackendApi1(component, adapter) {
private val globalLock = ReentrantLock()
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
new file mode 100644
index 0000000..ee9ff3c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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.layout.adapter.extensions
+
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.core.ConsumerAdapter
+import androidx.window.extensions.layout.WindowLayoutComponent
+import androidx.window.layout.SupportedPosture
+
+@RequiresWindowSdkExtension(version = 6)
+internal class ExtensionWindowBackendApi6(
+ component: WindowLayoutComponent,
+ adapter: ConsumerAdapter
+) : ExtensionWindowBackendApi2(component, adapter) {
+ override val supportedPostures: List<SupportedPosture>
+ get() = ExtensionsWindowLayoutInfoAdapter.translate(component.supportedWindowFeatures)
+}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
index 66cdf23..3ad07c2 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
@@ -21,7 +21,9 @@
import android.os.Build
import androidx.annotation.UiContext
import androidx.window.core.Bounds
+import androidx.window.extensions.layout.DisplayFoldFeature
import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
+import androidx.window.extensions.layout.SupportedWindowFeatures
import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
import androidx.window.layout.FoldingFeature
import androidx.window.layout.FoldingFeature.State.Companion.FLAT
@@ -29,9 +31,10 @@
import androidx.window.layout.HardwareFoldingFeature
import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetrics
-import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
+import androidx.window.layout.WindowMetricsCalculatorCompat
internal object ExtensionsWindowLayoutInfoAdapter {
@@ -63,10 +66,11 @@
@UiContext context: Context,
info: OEMWindowLayoutInfo,
): WindowLayoutInfo {
+ val calculator = WindowMetricsCalculatorCompat()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- translate(computeCurrentWindowMetrics(context), info)
+ translate(calculator.computeCurrentWindowMetrics(context), info)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (context is Activity)) {
- translate(computeCurrentWindowMetrics(context), info)
+ translate(calculator.computeCurrentWindowMetrics(context), info)
} else {
throw UnsupportedOperationException(
"Display Features are only supported after Q. Display features for non-Activity " +
@@ -89,6 +93,18 @@
return WindowLayoutInfo(features)
}
+ internal fun translate(features: SupportedWindowFeatures): List<SupportedPosture> {
+ val isTableTopSupported =
+ features.displayFoldFeatures.any { feature ->
+ feature.hasProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+ }
+ return if (isTableTopSupported) {
+ listOf(SupportedPosture.TABLETOP)
+ } else {
+ emptyList()
+ }
+ }
+
/**
* Checks the bounds for a [FoldingFeature] within a given [WindowMetrics]. Validates the
* following:
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
index 14a603c..31ee16f 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
@@ -23,6 +23,7 @@
import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
import androidx.window.core.Version
+import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.adapter.WindowBackend
import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface
@@ -127,6 +128,9 @@
}
}
+ override val supportedPostures: List<SupportedPosture>
+ get() = throw UnsupportedOperationException("Must be called from extensions.")
+
/**
* Checks if there are no more registered callbacks left for the activity and inform extension
* if needed.
diff --git a/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
new file mode 100644
index 0000000..3d24d92
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.Build
+import android.util.Log
+import android.view.Display
+import android.view.DisplayCutout
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
+import androidx.window.layout.util.BoundsHelper.Companion.TAG
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
+import java.lang.reflect.InvocationTargetException
+
+/** Provides compatibility behavior for calculating bounds of an activity. */
+internal interface BoundsHelper {
+
+ /** Compute the current bounds for the given [Activity]. */
+ fun currentWindowBounds(activity: Activity): Rect
+
+ fun maximumWindowBounds(@UiContext context: Context): Rect
+
+ companion object {
+
+ val TAG: String = BoundsHelper::class.java.simpleName
+
+ fun getInstance(): BoundsHelper {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
+ BoundsHelperApi30Impl
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
+ BoundsHelperApi29Impl
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
+ BoundsHelperApi28Impl
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
+ BoundsHelperApi24Impl
+ }
+ else -> {
+ BoundsHelperApi16Impl
+ }
+ }
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private object BoundsHelperApi30Impl : BoundsHelper {
+ override fun currentWindowBounds(activity: Activity): Rect {
+ val wm = activity.getSystemService(WindowManager::class.java)
+ return wm.currentWindowMetrics.bounds
+ }
+
+ override fun maximumWindowBounds(@UiContext context: Context): Rect {
+ val wm = context.getSystemService(WindowManager::class.java)
+ return wm.maximumWindowMetrics.bounds
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.Q)
+private object BoundsHelperApi29Impl : BoundsHelper {
+
+ /** Computes the window bounds for [Build.VERSION_CODES.Q]. */
+ @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
+ override fun currentWindowBounds(activity: Activity): Rect {
+ var bounds: Rect
+ val config = activity.resources.configuration
+ try {
+ val windowConfigField =
+ Configuration::class.java.getDeclaredField("windowConfiguration")
+ windowConfigField.isAccessible = true
+ val windowConfig = windowConfigField[config]
+ val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
+ bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchFieldException,
+ is NoSuchMethodException,
+ is IllegalAccessException,
+ is InvocationTargetException -> {
+ Log.w(TAG, e)
+ // If reflection fails for some reason default to the P implementation which
+ // still has the ability to account for display cutouts.
+ bounds = BoundsHelperApi28Impl.currentWindowBounds(activity)
+ }
+ else -> throw e
+ }
+ }
+ return bounds
+ }
+
+ override fun maximumWindowBounds(@UiContext context: Context): Rect {
+ return BoundsHelperApi28Impl.maximumWindowBounds(context)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+private object BoundsHelperApi28Impl : BoundsHelper {
+
+ /**
+ * Computes the window bounds for [Build.VERSION_CODES.P].
+ *
+ * NOTE: This method may result in incorrect values if the [android.content.res.Resources] value
+ * stored at 'navigation_bar_height' does not match the true navigation bar inset on the window.
+ */
+ @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
+ override fun currentWindowBounds(activity: Activity): Rect {
+ val bounds = Rect()
+ val config = activity.resources.configuration
+ try {
+ val windowConfigField =
+ Configuration::class.java.getDeclaredField("windowConfiguration")
+ windowConfigField.isAccessible = true
+ val windowConfig = windowConfigField[config]
+
+ // In multi-window mode we'll use the WindowConfiguration#mBounds property which
+ // should match the window size. Otherwise we'll use the mAppBounds property and
+ // will adjust it below.
+ if (isInMultiWindowMode(activity)) {
+ val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
+ bounds.set((getAppBounds.invoke(windowConfig) as Rect))
+ } else {
+ val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
+ bounds.set((getAppBounds.invoke(windowConfig) as Rect))
+ }
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchFieldException,
+ is NoSuchMethodException,
+ is IllegalAccessException,
+ is InvocationTargetException -> {
+ Log.w(TAG, e)
+ getRectSizeFromDisplay(activity, bounds)
+ }
+ else -> throw e
+ }
+ }
+
+ val platformWindowManager = activity.windowManager
+
+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") val currentDisplay = platformWindowManager.defaultDisplay
+ val realDisplaySize = Point()
+ @Suppress("DEPRECATION") currentDisplay.getRealSize(realDisplaySize)
+
+ if (!isInMultiWindowMode(activity)) {
+ // The activity is not in multi-window mode. Check if the addition of the
+ // navigation bar size to mAppBounds results in the real display size and if so
+ // assume the nav bar height should be added to the result.
+ val navigationBarHeight = getNavigationBarHeight(activity)
+ if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
+ bounds.bottom += navigationBarHeight
+ } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
+ bounds.right += navigationBarHeight
+ } else if (bounds.left == navigationBarHeight) {
+ bounds.left = 0
+ }
+ }
+ if (
+ (bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
+ !isInMultiWindowMode(activity)
+ ) {
+ // If the corrected bounds are not the same as the display size and the activity is
+ // not in multi-window mode it is possible there are unreported cutouts inset-ing
+ // the window depending on the layoutInCutoutMode. Check for them here by getting
+ // the cutout from the display itself.
+ val displayCutout = getCutoutForDisplay(currentDisplay)
+ if (displayCutout != null) {
+ if (bounds.left == safeInsetLeft(displayCutout)) {
+ bounds.left = 0
+ }
+ if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
+ bounds.right += safeInsetRight(displayCutout)
+ }
+ if (bounds.top == safeInsetTop(displayCutout)) {
+ bounds.top = 0
+ }
+ if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
+ bounds.bottom += safeInsetBottom(displayCutout)
+ }
+ }
+ }
+ return bounds
+ }
+
+ override fun maximumWindowBounds(@UiContext context: Context): Rect {
+ return BoundsHelperApi24Impl.maximumWindowBounds(context)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.N)
+private object BoundsHelperApi24Impl : BoundsHelper {
+
+ /**
+ * Computes the window bounds for platforms between [Build.VERSION_CODES.N] and
+ * [Build.VERSION_CODES.O_MR1], inclusive.
+ *
+ * NOTE: This method may result in incorrect values under the following conditions:
+ * * If the activity is in multi-window mode the origin of the returned bounds will always be
+ * anchored at (0, 0).
+ * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does not
+ * match the true navigation bar size the returned bounds will not take into account the
+ * navigation bar.
+ */
+ override fun currentWindowBounds(activity: Activity): Rect {
+ val bounds = Rect()
+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+ // [Display#getRectSize] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+ if (!isInMultiWindowMode(activity)) {
+ // The activity is not in multi-window mode. Check if the addition of the
+ // navigation bar size to Display#getSize() results in the real display size and
+ // if so return this value. If not, return the result of Display#getSize().
+ val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
+ val navigationBarHeight = getNavigationBarHeight(activity)
+ if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
+ bounds.bottom += navigationBarHeight
+ } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
+ bounds.right += navigationBarHeight
+ }
+ }
+ return bounds
+ }
+
+ override fun maximumWindowBounds(@UiContext context: Context): Rect {
+ return BoundsHelperApi16Impl.maximumWindowBounds(context)
+ }
+}
+
+private object BoundsHelperApi16Impl : BoundsHelper {
+
+ /**
+ * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN] and
+ * [Build.VERSION_CODES.M], inclusive.
+ *
+ * Given that multi-window mode isn't supported before N we simply return the real display size
+ * which should match the window size of a full-screen app.
+ */
+ override fun currentWindowBounds(activity: Activity): Rect {
+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+ val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
+ val bounds = Rect()
+ if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
+ // [Display#getRectSize] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+ } else {
+ bounds.right = realDisplaySize.x
+ bounds.bottom = realDisplaySize.y
+ }
+ return bounds
+ }
+
+ override fun maximumWindowBounds(@UiContext context: Context): Rect {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+ // compatibility with older versions, as we can't reliably get the display associated
+ // with a Context through public APIs either.
+ @Suppress("DEPRECATION") val display = wm.defaultDisplay
+ val displaySize = getRealSizeForDisplay(display)
+ return Rect(0, 0, displaySize.x, displaySize.y)
+ }
+}
+
+/**
+ * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
+ *
+ * Note: This is error-prone and is **not** the recommended way to determine the size of the
+ * overlapping region between the navigation bar and a given window. The best approach is to acquire
+ * the [android.view.WindowInsets].
+ */
+private fun getNavigationBarHeight(context: Context): Int {
+ val resources = context.resources
+ val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
+ return if (resourceId > 0) {
+ resources.getDimensionPixelSize(resourceId)
+ } else 0
+}
+
+private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+ // [Display#getRectSize] is deprecated but we have this for
+ // compatibility with older versions
+ @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+}
+
+/**
+ * Returns the [DisplayCutout] for the given display. Note that display cutout returned here is for
+ * the display and the insets provided are in the display coordinate system.
+ *
+ * @return the display cutout for the given display.
+ */
+@SuppressLint("BanUncheckedReflection")
+@RequiresApi(Build.VERSION_CODES.P)
+private fun getCutoutForDisplay(display: Display): DisplayCutout? {
+ var displayCutout: DisplayCutout? = null
+ try {
+ val displayInfoClass = Class.forName("android.view.DisplayInfo")
+ val displayInfoConstructor = displayInfoClass.getConstructor()
+ displayInfoConstructor.isAccessible = true
+ val displayInfo = displayInfoConstructor.newInstance()
+ val getDisplayInfoMethod =
+ display.javaClass.getDeclaredMethod("getDisplayInfo", displayInfo.javaClass)
+ getDisplayInfoMethod.isAccessible = true
+ getDisplayInfoMethod.invoke(display, displayInfo)
+ val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
+ displayCutoutField.isAccessible = true
+ val cutout = displayCutoutField[displayInfo]
+ if (cutout is DisplayCutout) {
+ displayCutout = cutout
+ }
+ } catch (e: Exception) {
+ when (e) {
+ is ClassNotFoundException,
+ is NoSuchMethodException,
+ is NoSuchFieldException,
+ is IllegalAccessException,
+ is InvocationTargetException,
+ is InstantiationException -> {
+ Log.w(TAG, e)
+ }
+ else -> throw e
+ }
+ }
+ return displayCutout
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
index 36fc60b..9207d98 100644
--- a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
+++ b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
@@ -19,14 +19,12 @@
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
-import android.graphics.Rect
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.annotation.UiContext
import androidx.core.view.WindowInsetsCompat
-import androidx.window.layout.WindowMetrics
internal object ContextCompatHelper {
/**
@@ -59,32 +57,9 @@
}
}
-@RequiresApi(Build.VERSION_CODES.N)
-internal object ContextCompatHelperApi24 {
- fun isInMultiWindowMode(activity: Activity): Boolean {
- return activity.isInMultiWindowMode
- }
-}
-
@RequiresApi(Build.VERSION_CODES.R)
internal object ContextCompatHelperApi30 {
- fun currentWindowMetrics(@UiContext context: Context): WindowMetrics {
- val wm = context.getSystemService(WindowManager::class.java)
- val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
- return WindowMetrics(wm.currentWindowMetrics.bounds, insets)
- }
-
- fun currentWindowBounds(@UiContext context: Context): Rect {
- val wm = context.getSystemService(WindowManager::class.java)
- return wm.currentWindowMetrics.bounds
- }
-
- fun maximumWindowBounds(@UiContext context: Context): Rect {
- val wm = context.getSystemService(WindowManager::class.java)
- return wm.maximumWindowMetrics.bounds
- }
-
/**
* Computes the [WindowInsetsCompat] for platforms above [Build.VERSION_CODES.R], inclusive.
*
diff --git a/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
new file mode 100644
index 0000000..96408ed
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+
+/** Provides compatibility behavior for functionality related to display density. */
+internal interface DensityCompatHelper {
+
+ /** Returns the logical density of the display associated with the [Context]. */
+ fun density(context: Context): Float
+
+ /**
+ * Returns the logical density of the display associated with the [Configuration] or
+ * [AndroidWindowMetrics], depending on the SDK level.
+ */
+ fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float
+
+ companion object {
+ fun getInstance(): DensityCompatHelper {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
+ DensityCompatHelperApi34Impl
+ else -> DensityCompatHelperBaseImpl
+ }
+ }
+ }
+}
+
+private object DensityCompatHelperBaseImpl : DensityCompatHelper {
+ override fun density(context: Context): Float {
+ return context.resources.displayMetrics.density
+ }
+
+ override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
+ return configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private object DensityCompatHelperApi34Impl : DensityCompatHelper {
+ override fun density(@UiContext context: Context): Float {
+ return context.getSystemService(WindowManager::class.java).currentWindowMetrics.density
+ }
+
+ override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
+ return windowMetrics.density
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
new file mode 100644
index 0000000..30463dc
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.graphics.Point
+import android.view.Display
+
+internal object DisplayHelper {
+
+ /**
+ * Returns the full (real) size of the display, in pixels, without subtracting any window decor
+ * or applying any compatibility scale factors.
+ *
+ * The size is adjusted based on the current rotation of the display.
+ *
+ * @return a point representing the real display size in pixels.
+ * @see Display.getRealSize
+ */
+ @Suppress("DEPRECATION")
+ fun getRealSizeForDisplay(display: Display): Point {
+ val size = Point()
+ display.getRealSize(size)
+ return size
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
new file mode 100644
index 0000000..313ebf6
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 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.layout.util
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Rect
+import android.inputmethodservice.InputMethodService
+import android.os.Build
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.core.Bounds
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
+import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
+import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
+
+/** Provides compatibility behavior for functionality related to [WindowMetricsCalculator]. */
+internal interface WindowMetricsCompatHelper {
+
+ /** Translates platform [AndroidWindowMetrics] to jetpack [WindowMetrics]. */
+ @RequiresApi(Build.VERSION_CODES.R)
+ fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics, density: Float): WindowMetrics
+
+ /** Returns the [WindowMetrics] associated with the provided [Context]. */
+ fun currentWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics
+
+ /** Returns the [WindowMetrics] associated with the provided [Activity]. */
+ fun currentWindowMetrics(
+ activity: Activity,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics
+
+ /** Returns the maximum [WindowMetrics] for a given [UiContext]. */
+ fun maximumWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics
+
+ companion object {
+ fun getInstance(): WindowMetricsCompatHelper {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
+ WindowMetricsCompatHelperApi34Impl
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> WindowMetricsCompatHelperApi30Impl
+ else -> WindowMetricsCompatHelperBaseImpl
+ }
+ }
+ }
+}
+
+internal object WindowMetricsCompatHelperBaseImpl : WindowMetricsCompatHelper {
+
+ override fun translateWindowMetrics(
+ windowMetrics: AndroidWindowMetrics,
+ density: Float
+ ): WindowMetrics {
+ throw UnsupportedOperationException("translateWindowMetrics not available before API30")
+ }
+
+ override fun currentWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ when (val unwrappedContext = unwrapUiContext(context)) {
+ is Activity -> {
+ return currentWindowMetrics(unwrappedContext, densityCompatHelper)
+ }
+ is InputMethodService -> {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ // On older SDK levels, the app and IME could show up on different displays.
+ // However, there isn't a way for us to figure this out from the application
+ // layer. But, this should be good enough for now given the small likelihood of
+ // IMEs showing up on non-primary displays on these SDK levels.
+ @Suppress("DEPRECATION") val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
+
+ // IME occupies the whole display bounds.
+ val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
+ return WindowMetrics(imeBounds, density = densityCompatHelper.density(context))
+ }
+ else -> {
+ throw IllegalArgumentException("$context is not a UiContext")
+ }
+ }
+ }
+
+ override fun currentWindowMetrics(
+ activity: Activity,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ // TODO (b/233899790): compute insets for other platform versions below R
+ return WindowMetrics(
+ Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
+ WindowInsetsCompat.Builder().build(),
+ densityCompatHelper.density(activity)
+ )
+ }
+
+ override fun maximumWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ // TODO (b/233899790): compute insets for other platform versions below Rs
+ return WindowMetrics(
+ Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
+ WindowInsetsCompat.Builder().build(),
+ densityCompatHelper.density(context)
+ )
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+internal object WindowMetricsCompatHelperApi30Impl : WindowMetricsCompatHelper {
+
+ override fun translateWindowMetrics(
+ windowMetrics: AndroidWindowMetrics,
+ density: Float
+ ): WindowMetrics {
+ return WindowMetrics(
+ windowMetrics.bounds,
+ WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
+ density
+ )
+ }
+
+ override fun currentWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ val wm = context.getSystemService(WindowManager::class.java)
+ val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
+ val density = context.resources.displayMetrics.density
+ return WindowMetrics(wm.currentWindowMetrics.bounds, insets, density)
+ }
+
+ override fun currentWindowMetrics(
+ activity: Activity,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ return WindowMetrics(
+ Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
+ currentWindowInsets(activity),
+ densityCompatHelper.density(activity)
+ )
+ }
+
+ override fun maximumWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ return WindowMetrics(
+ Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
+ currentWindowInsets(context),
+ densityCompatHelper.density(context)
+ )
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+internal object WindowMetricsCompatHelperApi34Impl : WindowMetricsCompatHelper {
+
+ override fun translateWindowMetrics(
+ windowMetrics: AndroidWindowMetrics,
+ density: Float
+ ): WindowMetrics {
+ return WindowMetrics(
+ windowMetrics.bounds,
+ WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
+ windowMetrics.density
+ )
+ }
+
+ override fun currentWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ val wm = context.getSystemService(WindowManager::class.java)
+ return WindowMetrics(
+ wm.currentWindowMetrics.bounds,
+ WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets),
+ wm.currentWindowMetrics.density
+ )
+ }
+
+ override fun currentWindowMetrics(
+ activity: Activity,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ return WindowMetricsCompatHelperApi30Impl.currentWindowMetrics(
+ activity,
+ densityCompatHelper
+ )
+ }
+
+ override fun maximumWindowMetrics(
+ @UiContext context: Context,
+ densityCompatHelper: DensityCompatHelper
+ ): WindowMetrics {
+ return WindowMetricsCompatHelperApi30Impl.maximumWindowMetrics(context, densityCompatHelper)
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
index d437ab35..71bfe6a 100644
--- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
+++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
@@ -18,6 +18,7 @@
import android.util.Log
import java.lang.reflect.Constructor
+import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import kotlin.reflect.KClass
@@ -42,10 +43,10 @@
* result from the [block]
*/
@JvmStatic
- internal fun validateReflection(errorMessage: String? = null, block: () -> Boolean): Boolean {
+ internal fun validateReflection(errorMessage: String, block: () -> Boolean): Boolean {
return try {
val result = block()
- if (!result && errorMessage != null) {
+ if (!result) {
Log.e("ReflectionGuard", errorMessage)
}
result
@@ -55,6 +56,9 @@
} catch (noMethod: NoSuchMethodException) {
Log.e("ReflectionGuard", "NoSuchMethod: ${errorMessage.orEmpty()}")
false
+ } catch (noField: NoSuchFieldException) {
+ Log.e("ReflectionGuard", "NoSuchField: ${errorMessage.orEmpty()}")
+ false
}
}
@@ -70,6 +74,12 @@
return Modifier.isPublic(modifiers)
}
+ /** Checks if a field has public modifier */
+ internal val Field.isPublic: Boolean
+ get() {
+ return Modifier.isPublic(modifiers)
+ }
+
/** Checks if a method's return value is type of kotlin [clazz] */
internal fun Method.doesReturn(clazz: KClass<*>): Boolean {
return doesReturn(clazz.java)
diff --git a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
index dc2bfd8..1a72e56 100644
--- a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
+++ b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
@@ -31,19 +31,29 @@
/** Constant name for class [androidx.window.extensions.WindowExtensions] used for reflection */
internal const val WINDOW_EXTENSIONS_CLASS = "$WINDOW_EXTENSIONS_PACKAGE_NAME.WindowExtensions"
+ internal const val LAYOUT_PACKAGE = "layout"
+
/**
* Constant name for class [androidx.window.extensions.layout.FoldingFeature] used for
* reflection
*/
internal const val FOLDING_FEATURE_CLASS =
- "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.FoldingFeature"
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.FoldingFeature"
+
+ /** Constant name for class [androidx.window.extensions.layout.SupportedWindowFeatures] */
+ internal const val SUPPORTED_WINDOW_FEATURES_CLASS =
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.SupportedWindowFeatures"
+
+ /** Constant name for class [androidx.window.extensions.layout.DisplayFoldFeature] */
+ internal const val DISPLAY_FOLD_FEATURE_CLASS =
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.DisplayFoldFeature"
/**
* Constant name for class [androidx.window.extensions.layout.WindowLayoutComponent] used for
* reflection
*/
internal const val WINDOW_LAYOUT_COMPONENT_CLASS =
- "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.WindowLayoutComponent"
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.WindowLayoutComponent"
/**
* Constant name for class [androidx.window.extensions.area.WindowAreaComponent] used for
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index 5342612..04a4c09 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -43,7 +43,7 @@
`ActivityRule`. The suggested usage is to set the tag to be able to differentiate between
different rules in the callbacks.
For example, it can be used to compute the right `SplitAttributes` for the given split rule
- in `SplitAttributes` calculator function. -->
+ in `SplitAttributesCalculator.computeSplitAttributesForParams`. -->
<attr name="tag" format="string" />
<!-- An attribute for Activity Embedding rules.
Background color of Activity Embedding window animation if the animation requires a
@@ -51,6 +51,31 @@
The default is to use the current theme's windowBackground. Any non-opaque color will be
treated the same as default. -->
<attr name="animationBackgroundColor" format="color" />
+ <!-- An attribute for Activity Embedding rules.
+ Defines the open animation transition spec for the Activity Embedding split. -->
+ <attr name="splitOpenAnimation" format="enum">
+ <!-- The default animation specified by the system. -->
+ <enum name="systemDefault" value="0" />
+ <!-- An animation with zero duration. -->
+ <enum name="jumpCut" value="1" />
+ </attr>
+ <!-- An attribute for Activity Embedding rules.
+ Defines the close animation transition spec for the Activity Embedding split. -->
+ <attr name="splitCloseAnimation" format="enum">
+ <!-- The default animation specified by the system. -->
+ <enum name="systemDefault" value="0" />
+ <!-- An animation with zero duration. -->
+ <enum name="jumpCut" value="1" />
+ </attr>
+ <!-- An attribute for Activity Embedding rules.
+ Defines the change (resize or move) animation transition spec for the Activity Embedding
+ split. -->
+ <attr name="splitChangeAnimation" format="enum">
+ <!-- The default animation specified by the system. -->
+ <enum name="systemDefault" value="0" />
+ <!-- An animation with zero duration. -->
+ <enum name="jumpCut" value="1" />
+ </attr>
<!-- An attribute for Activity Embedding rules.
The smallest value of width of the parent Task bounds when the split should be used, in DP.
@@ -161,6 +186,9 @@
<attr name="splitLayoutDirection"/>
<attr name="tag"/>
<attr name="animationBackgroundColor"/>
+ <attr name="splitOpenAnimation"/>
+ <attr name="splitCloseAnimation"/>
+ <attr name="splitChangeAnimation"/>
</declare-styleable>
<!-- Attributes that are read when parsing a <SplitPlaceholderRule> tag, which defines the split
@@ -187,6 +215,9 @@
<attr name="splitLayoutDirection"/>
<attr name="tag"/>
<attr name="animationBackgroundColor"/>
+ <attr name="splitOpenAnimation"/>
+ <attr name="splitCloseAnimation"/>
+ <attr name="splitChangeAnimation"/>
</declare-styleable>
<!-- Attributes that are read when parsing an <ActivityRule> tag, which defines the layout
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
index 6f671e9..6a414bf 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
@@ -18,19 +18,33 @@
import android.app.Activity
import android.content.Context
-import androidx.window.core.ExperimentalWindowApi
+import android.graphics.Rect
+import androidx.core.util.Consumer
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
/** The unit tests for [ActivityEmbeddingController]. */
+@OptIn(ExperimentalCoroutinesApi::class)
class ActivityEmbeddingControllerTest {
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
private lateinit var mockEmbeddingBackend: EmbeddingBackend
private lateinit var mockContext: Context
private lateinit var mockActivity: Activity
@@ -58,11 +72,73 @@
}
@Test
- @OptIn(ExperimentalWindowApi::class)
fun testGetActivityStack() {
val activityStack = ActivityStack(listOf(), true)
whenever(mockEmbeddingBackend.getActivityStack(mockActivity)).thenReturn(activityStack)
assertEquals(activityStack, activityEmbeddingController.getActivityStack(mockActivity))
}
+
+ @Test
+ fun testFinishActivityStacks() {
+ val activityStacks: Set<ActivityStack> = mock()
+ activityEmbeddingController.finishActivityStacks(activityStacks)
+
+ verify(mockEmbeddingBackend).finishActivityStacks(activityStacks)
+ }
+
+ @Test
+ fun test_invalidateTopVisibleSplitAttributes_delegates() {
+ activityEmbeddingController.invalidateVisibleActivityStacks()
+ verify(mockEmbeddingBackend).invalidateVisibleActivityStacks()
+ }
+
+ @Test
+ fun test_embeddedActivityWindowInfo_delegates() =
+ testScope.runTest {
+ val expectedInfo =
+ EmbeddedActivityWindowInfo(
+ isEmbedded = true,
+ parentHostBounds = Rect(0, 0, 1000, 2000),
+ boundsInParentHost = Rect(0, 0, 500, 2000)
+ )
+ doAnswer { invocationOnMock ->
+ @Suppress("UNCHECKED_CAST")
+ val callback =
+ invocationOnMock.arguments.last() as Consumer<EmbeddedActivityWindowInfo>
+ callback.accept(expectedInfo)
+ }
+ .whenever(mockEmbeddingBackend)
+ .addEmbeddedActivityWindowInfoCallbackForActivity(any(), any())
+
+ val actualInfo =
+ activityEmbeddingController
+ .embeddedActivityWindowInfo(mockActivity)
+ .take(1)
+ .toList()
+ .first()
+
+ assertEquals(expectedInfo, actualInfo)
+ verify(mockEmbeddingBackend)
+ .addEmbeddedActivityWindowInfoCallbackForActivity(eq(mockActivity), any())
+ verify(mockEmbeddingBackend).removeEmbeddedActivityWindowInfoCallbackForActivity(any())
+ }
+
+ @Test
+ fun testGetInstance() {
+ EmbeddingBackend.overrideDecorator(
+ object : EmbeddingBackendDecorator {
+ override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
+ mockEmbeddingBackend
+ }
+ )
+ val controller = ActivityEmbeddingController.getInstance(mockActivity)
+ val activityStacks: Set<ActivityStack> = mock()
+
+ controller.finishActivityStacks(activityStacks)
+
+ verify(mockEmbeddingBackend).finishActivityStacks(activityStacks)
+
+ EmbeddingBackend.reset()
+ }
}
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
new file mode 100644
index 0000000..53fca91
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * The unit tests for activity embedding extension functions to [Bundle]
+ *
+ * @see Bundle.setLaunchingActivityStack
+ * @see Bundle.setOverlayCreateParams
+ */
+class ActivityEmbeddingOptionsTest {
+
+ @Mock private lateinit var mockEmbeddingBackend: EmbeddingBackend
+ @Mock private lateinit var mockContext: Context
+ @Mock private lateinit var mockActivity: Activity
+ @Mock private lateinit var mockOptions: Bundle
+
+ private lateinit var annotationClosable: AutoCloseable
+
+ private lateinit var mockActivityStack: ActivityStack
+
+ @Before
+ fun setUp() {
+ annotationClosable = MockitoAnnotations.openMocks(this)
+
+ mockActivityStack =
+ ActivityStack(listOf(), true, ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN)
+ whenever(mockActivity.applicationContext).doReturn(mockContext)
+
+ EmbeddingBackend.overrideDecorator(
+ object : EmbeddingBackendDecorator {
+ override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
+ mockEmbeddingBackend
+ }
+ )
+ }
+
+ @After
+ fun tearDown() {
+ EmbeddingBackend.reset()
+ annotationClosable.close()
+ }
+
+ @Test
+ fun testSetLaunchingActivityStack() {
+ mockOptions.setLaunchingActivityStack(mockActivity, mockActivityStack)
+
+ verify(mockEmbeddingBackend).setLaunchingActivityStack(mockOptions, mockActivityStack)
+ }
+
+ @Test
+ fun testSetOverlayCreateParams() {
+ val overlayCreateParams = OverlayCreateParams(overlayAttributes = OverlayAttributes())
+ mockOptions.setOverlayCreateParams(mockActivity, overlayCreateParams)
+
+ verify(mockEmbeddingBackend).setOverlayCreateParams(mockOptions, overlayCreateParams)
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
index 7c50304..203e1ff 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
@@ -17,8 +17,11 @@
package androidx.window.embedding
import android.app.Activity
+import android.os.Binder
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.kotlin.mock
@@ -37,11 +40,18 @@
@Test
fun testEqualsImpliesHashCode() {
val activity = mock<Activity>()
- val first = ActivityStack(listOf(activity), isEmpty = false)
- val second = ActivityStack(listOf(activity), isEmpty = false)
+ val token = ActivityStackToken.createFromBinder(Binder())
+ val first = ActivityStack(listOf(activity), isEmpty = false, token)
+ val second = ActivityStack(listOf(activity), isEmpty = false, token)
assertEquals(first, second)
assertEquals(first.hashCode(), second.hashCode())
+
+ val anotherToken = ActivityStackToken.createFromBinder(Binder())
+ val third = ActivityStack(emptyList(), isEmpty = true, anotherToken)
+
+ assertNotEquals(first, third)
+ assertNotEquals(first.hashCode(), third.hashCode())
}
@Test
@@ -59,10 +69,12 @@
fun testToString() {
val activitiesInProcess = mock<List<Activity>>()
val isEmpty = false
+ val token = ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
- val stackString = ActivityStack(activitiesInProcess, isEmpty).toString()
+ val stackString = ActivityStack(activitiesInProcess, isEmpty, token).toString()
assertTrue(stackString.contains(activitiesInProcess.toString()))
assertTrue(stackString.contains(isEmpty.toString()))
+ assertTrue(stackString.contains(token.toString()))
}
}
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt
new file mode 100644
index 0000000..1b90cb5
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.app.Activity
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.WindowSdkExtensionsRule
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Verifies [ActivityWindowInfoCallbackController] */
+@Suppress("GuardedBy")
+class ActivityWindowInfoCallbackControllerTest {
+
+ @get:Rule val testRule = WindowSdkExtensionsRule()
+
+ @Mock private lateinit var embeddingExtension: ActivityEmbeddingComponent
+ @Mock private lateinit var callback1: JetpackConsumer<EmbeddedActivityWindowInfo>
+ @Mock private lateinit var callback2: JetpackConsumer<EmbeddedActivityWindowInfo>
+ @Mock private lateinit var activity1: Activity
+ @Mock private lateinit var activity2: Activity
+
+ @Captor
+ private lateinit var callbackCaptor: ArgumentCaptor<Consumer<ExtensionsActivityWindowInfo>>
+
+ private lateinit var controller: ActivityWindowInfoCallbackController
+ private lateinit var mockAnnotations: AutoCloseable
+
+ @Before
+ fun setUp() {
+ testRule.overrideExtensionVersion(6)
+ mockAnnotations = MockitoAnnotations.openMocks(this)
+
+ controller = ActivityWindowInfoCallbackController(embeddingExtension)
+ // ArrayMap is not available in JVM test
+ controller.callbacks = HashMap()
+ controller = spy(controller)
+ }
+
+ @After
+ fun tearDown() {
+ mockAnnotations.close()
+ }
+
+ @Test
+ fun testAddCallback() {
+ // Register the extensions callback
+ controller.addCallback(activity1, callback1)
+
+ verify(embeddingExtension).setEmbeddedActivityWindowInfoCallback(any(), any())
+
+ // Do not register for the additional callback
+ clearInvocations(embeddingExtension)
+ controller.addCallback(activity1, callback2)
+
+ verify(embeddingExtension, never()).setEmbeddedActivityWindowInfoCallback(any(), any())
+ }
+
+ @Test
+ fun testRemoveCallback() {
+ // Unregister after the last Jetpack callback is removed.
+ controller.addCallback(activity1, callback1)
+ controller.addCallback(activity1, callback2)
+
+ controller.removeCallback(callback1)
+
+ verify(embeddingExtension, never()).clearEmbeddedActivityWindowInfoCallback()
+
+ controller.removeCallback(callback2)
+
+ verify(embeddingExtension).clearEmbeddedActivityWindowInfoCallback()
+ }
+
+ @Test
+ fun testActivityWindowInfoChanged() {
+ controller.addCallback(activity1, callback1)
+ controller.addCallback(activity1, callback2)
+
+ verify(embeddingExtension)
+ .setEmbeddedActivityWindowInfoCallback(any(), callbackCaptor.capture())
+ val extensionsCallback = callbackCaptor.value
+
+ val extensionsInfo: ExtensionsActivityWindowInfo = mock()
+ val expectedInfo =
+ EmbeddedActivityWindowInfo(
+ isEmbedded = true,
+ parentHostBounds = mock(),
+ boundsInParentHost = mock()
+ )
+ doReturn(expectedInfo).whenever(controller).translate(extensionsInfo)
+
+ // No callback because the activity doesn't match.
+ doReturn(activity2).whenever(extensionsInfo).activity
+ extensionsCallback.accept(extensionsInfo)
+
+ verify(callback1, never()).accept(any())
+ verify(callback2, never()).accept(any())
+
+ // Should receive the correct translated info.
+ doReturn(activity1).whenever(extensionsInfo).activity
+ extensionsCallback.accept(extensionsInfo)
+
+ verify(callback1).accept(expectedInfo)
+ verify(callback2).accept(expectedInfo)
+
+ // Do not send unchanged info.
+ clearInvocations(callback1)
+ clearInvocations(callback2)
+ extensionsCallback.accept(extensionsInfo)
+
+ verify(callback1, never()).accept(any())
+ verify(callback2, never()).accept(any())
+
+ // Only the remaining callback can receive the info.
+ controller.removeCallback(callback1)
+ val expectedInfo2 =
+ EmbeddedActivityWindowInfo(
+ isEmbedded = false,
+ parentHostBounds = mock(),
+ boundsInParentHost = mock()
+ )
+ doReturn(expectedInfo2).whenever(controller).translate(extensionsInfo)
+ extensionsCallback.accept(extensionsInfo)
+
+ verify(callback1, never()).accept(any())
+ verify(callback2).accept(any())
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt
new file mode 100644
index 0000000..1676367
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 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.embedding
+
+import android.graphics.Color
+import androidx.window.embedding.DividerAttributes.DragRange.Companion.DRAG_RANGE_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+/** Test class to verify [DividerAttributes] */
+class DividerAttributesTest {
+
+ @Test
+ fun testDividerAttributesEquals() {
+ val attrs1 = FixedDividerAttributes.Builder().build()
+ val attrs2 = DraggableDividerAttributes.Builder().build()
+ val attrs3 =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(20)
+ .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+ .build()
+ val attrs4 =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(DividerAttributes.WIDTH_SYSTEM_DEFAULT)
+ .setDragRange(DRAG_RANGE_SYSTEM_DEFAULT)
+ .build()
+ val attrs5 =
+ DraggableDividerAttributes.Builder(attrs4).setDraggingToFullscreenAllowed(true).build()
+
+ assertNotEquals(attrs1, attrs2)
+ assertNotEquals(attrs1.hashCode(), attrs2.hashCode())
+
+ assertNotEquals(attrs2, attrs3)
+ assertNotEquals(attrs2.hashCode(), attrs3.hashCode())
+
+ assertNotEquals(attrs3, attrs4)
+ assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
+
+ assertNotEquals(attrs4, attrs5)
+ assertNotEquals(attrs4.hashCode(), attrs5.hashCode())
+
+ // attrs2 and attrs4 must be equal because attrs4 uses default values.
+ assertEquals(attrs2, attrs4)
+ assertEquals(attrs2.hashCode(), attrs4.hashCode())
+ }
+
+ @Test
+ fun testBuilder() {
+ val attrs =
+ DraggableDividerAttributes.Builder()
+ .setWidthDp(20)
+ .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+ .setColor(Color.GRAY)
+ .setDraggingToFullscreenAllowed(true)
+ .build()
+
+ assertEquals(20, attrs.widthDp)
+ assertEquals(SplitRatioDragRange(0.3f, 0.7f), attrs.dragRange)
+ assertEquals(Color.GRAY, attrs.color)
+ assertEquals(true, attrs.isDraggingToFullscreenAllowed)
+ }
+
+ @Test
+ fun testBuilder_defaultValues() {
+ val attrs = DraggableDividerAttributes.Builder().build()
+
+ assertEquals(DividerAttributes.WIDTH_SYSTEM_DEFAULT, attrs.widthDp)
+ assertEquals(DRAG_RANGE_SYSTEM_DEFAULT, attrs.dragRange)
+ assertEquals(Color.BLACK, attrs.color)
+ assertEquals(false, attrs.isDraggingToFullscreenAllowed)
+ }
+
+ @Test
+ fun testSplitRatioDragRange_validation() {
+ // Valid range
+ SplitRatioDragRange(minRatio = 0.2f, maxRatio = 0.8f)
+
+ // Invalid minRatio and maxRatio
+ assertThrows(IllegalArgumentException::class.java) {
+ SplitRatioDragRange(minRatio = 0.0f, maxRatio = 1.0f)
+ }
+
+ // Invalid minRatio
+ assertThrows(IllegalArgumentException::class.java) {
+ SplitRatioDragRange(minRatio = -1.0f, maxRatio = 0.8f)
+ }
+
+ // Invalid maxRatio
+ assertThrows(IllegalArgumentException::class.java) {
+ SplitRatioDragRange(minRatio = 0.2f, maxRatio = 1.2f)
+ }
+
+ // minRatio should not be less than maxRatio
+ assertThrows(IllegalArgumentException::class.java) {
+ SplitRatioDragRange(minRatio = 0.6f, maxRatio = 0.4f)
+ }
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt b/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt
new file mode 100644
index 0000000..d5df40d
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.graphics.Rect
+import androidx.window.core.Bounds
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [EmbeddingBounds] */
+class EmbeddingBoundsTests {
+
+ private val taskBounds = Bounds(0, 0, 10, 10)
+
+ private val layoutInfoWithoutHinge = WindowLayoutInfo(emptyList())
+
+ private val layoutInfoWithTwoHinges =
+ WindowLayoutInfo(
+ listOf(
+ TestFoldingFeature(Bounds(left = 4, top = 0, right = 6, bottom = 10)),
+ TestFoldingFeature(Bounds(left = 0, top = 4, right = 10, bottom = 6)),
+ )
+ )
+
+ private val layoutInfoWithVerticalHinge =
+ WindowLayoutInfo(
+ listOf(TestFoldingFeature(Bounds(left = 4, top = 0, right = 6, bottom = 10)))
+ )
+
+ private val layoutInfoWithHorizontalHinge =
+ WindowLayoutInfo(
+ listOf(TestFoldingFeature(Bounds(left = 0, top = 4, right = 10, bottom = 6)))
+ )
+
+ @Test
+ fun testTranslateBOUNDS_EXPANDED_returnEmptyBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_EXPANDED,
+ taskBounds,
+ layoutInfoWithVerticalHinge
+ )
+ .isZero
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testTranslateBoundsMatchParentTask_returnEmptyBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_TOP,
+ width = EmbeddingBounds.Dimension.pixel(taskBounds.width),
+ height = EmbeddingBounds.Dimension.pixel(taskBounds.height),
+ ),
+ taskBounds,
+ layoutInfoWithVerticalHinge
+ )
+ .isZero
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testTranslateBoundsLargerThanParentTask_returnEmptyBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_TOP,
+ width = EmbeddingBounds.Dimension.pixel(taskBounds.width + 1),
+ height = EmbeddingBounds.Dimension.pixel(taskBounds.height + 1),
+ ),
+ taskBounds,
+ layoutInfoWithVerticalHinge
+ )
+ .isZero
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testTranslateBOUNDS_HINGE_LEFT() {
+ val fallbackBoundsHingeLeft = Bounds(left = 0, top = 0, right = 5, bottom = 10)
+
+ assertWithMessage("Must fallback to the left half on device without hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_LEFT,
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeLeft)
+
+ assertWithMessage("Must fallback to the left half on device with multiple hinges")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_LEFT,
+ taskBounds,
+ layoutInfoWithTwoHinges,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeLeft)
+
+ assertWithMessage("Must fallback to the left half on device with a horizontal hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_LEFT,
+ taskBounds,
+ layoutInfoWithHorizontalHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeLeft)
+
+ assertWithMessage("Must report bounds on the left of hinge on device with a vertical hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_LEFT,
+ taskBounds,
+ layoutInfoWithVerticalHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 0, top = 0, right = 4, bottom = 10))
+ }
+
+ @Test
+ fun testTranslateBOUNDS_HINGE_TOP() {
+ val fallbackBoundsHingeTop = Bounds(left = 0, top = 0, right = 10, bottom = 5)
+
+ assertWithMessage("Must fallback to the top half on device without hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_TOP,
+ taskBounds,
+ layoutInfoWithoutHinge
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeTop)
+
+ assertWithMessage("Must fallback to the top half on device with multiple hinges")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_TOP,
+ taskBounds,
+ layoutInfoWithTwoHinges,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeTop)
+
+ assertWithMessage("Must fallback to the top half on device with a vertical hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_TOP,
+ taskBounds,
+ layoutInfoWithVerticalHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeTop)
+
+ assertWithMessage(
+ "Must report bounds on the top of hinge on device with a horizontal hinge"
+ )
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_TOP,
+ taskBounds,
+ layoutInfoWithHorizontalHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 0, top = 0, right = 10, bottom = 4))
+ }
+
+ @Test
+ fun testTranslateBOUNDS_HINGE_RIGHT() {
+ val fallbackBoundsHingeRight = Bounds(left = 5, top = 0, right = 10, bottom = 10)
+
+ assertWithMessage("Must fallback to the right half on device without hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeRight)
+
+ assertWithMessage("Must fallback to the right half on device with multiple hinges")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+ taskBounds,
+ layoutInfoWithTwoHinges,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeRight)
+
+ assertWithMessage("Must fallback to the right half on device with a horizontal hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+ taskBounds,
+ layoutInfoWithHorizontalHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeRight)
+
+ assertWithMessage(
+ "Must report bounds on the right of hinge on device with a vertical hinge"
+ )
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+ taskBounds,
+ layoutInfoWithVerticalHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 6, top = 0, right = 10, bottom = 10))
+ }
+
+ @Test
+ fun testTranslateBOUNDS_HINGE_BOTTOM() {
+ val fallbackBoundsHingeBottom = Bounds(left = 0, top = 5, right = 10, bottom = 10)
+
+ assertWithMessage("Must fallback to the bottom half on device without hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeBottom)
+
+ assertWithMessage("Must fallback to the bottom half on device with multiple hinges")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+ taskBounds,
+ layoutInfoWithTwoHinges,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeBottom)
+
+ assertWithMessage("Must fallback to the bottom half on device with a vertical hinge")
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+ taskBounds,
+ layoutInfoWithVerticalHinge,
+ )
+ )
+ .isEqualTo(fallbackBoundsHingeBottom)
+
+ assertWithMessage(
+ "Must report bounds on the bottom of hinge on device with a horizontal hinge"
+ )
+ .that(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+ taskBounds,
+ layoutInfoWithHorizontalHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 0, top = 6, right = 10, bottom = 10))
+ }
+
+ @Test
+ fun testTranslateShrunkLeftBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_LEFT,
+ EmbeddingBounds.Dimension.ratio(0.7f),
+ EmbeddingBounds.Dimension.pixel(8),
+ ),
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 0, top = 1, right = 7, bottom = 9))
+ }
+
+ @Test
+ fun testTranslateShrunkTopBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_TOP,
+ EmbeddingBounds.Dimension.pixel(8),
+ EmbeddingBounds.Dimension.ratio(0.5f),
+ ),
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 1, top = 0, right = 9, bottom = 5))
+ }
+
+ @Test
+ fun testTranslateShrunkBottomBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+ EmbeddingBounds.Dimension.DIMENSION_HINGE,
+ EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+ ),
+ taskBounds,
+ layoutInfoWithoutHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 2, top = 0, right = 7, bottom = 10))
+ }
+
+ @Test
+ fun testTranslateShrunkRightBounds() {
+ assertThat(
+ EmbeddingBounds.translateEmbeddingBounds(
+ EmbeddingBounds(
+ EmbeddingBounds.Alignment.ALIGN_RIGHT,
+ EmbeddingBounds.Dimension.DIMENSION_HINGE,
+ EmbeddingBounds.Dimension.pixel(4),
+ ),
+ taskBounds,
+ layoutInfoWithVerticalHinge,
+ )
+ )
+ .isEqualTo(Bounds(left = 6, top = 3, right = 10, bottom = 7))
+ }
+
+ private class TestFoldingFeature(val rawBounds: Bounds) : FoldingFeature {
+ override val bounds: Rect
+ get() =
+ mock<Rect>().apply {
+ left = rawBounds.left
+ top = rawBounds.top
+ right = rawBounds.right
+ bottom = rawBounds.bottom
+ doReturn(rawBounds.width).whenever(this).width()
+ doReturn(rawBounds.height).whenever(this).height()
+ }
+
+ override val isSeparating: Boolean
+ get() = true
+
+ override val occlusionType: FoldingFeature.OcclusionType
+ get() = FoldingFeature.OcclusionType.FULL
+
+ override val orientation: FoldingFeature.Orientation
+ get() =
+ if (rawBounds.width > rawBounds.height) {
+ FoldingFeature.Orientation.HORIZONTAL
+ } else {
+ FoldingFeature.Orientation.VERTICAL
+ }
+
+ override val state: FoldingFeature.State
+ get() = FoldingFeature.State.FLAT
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt b/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
index 03df207..4f88743 100644
--- a/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
@@ -17,12 +17,14 @@
package androidx.window.embedding
import android.app.Activity
+import androidx.window.WindowSdkExtensions
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import java.util.concurrent.Executor
import java.util.function.Consumer as JavaConsumer
import org.junit.Test
import org.mockito.kotlin.any
@@ -32,9 +34,16 @@
class EmbeddingCompatTest {
private val component = mock<ActivityEmbeddingComponent>()
- private val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
+ private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
private val embeddingCompat =
- EmbeddingCompat(component, EMBEDDING_ADAPTER, CONSUMER_ADAPTER, mock())
+ EmbeddingCompat(
+ component,
+ EMBEDDING_ADAPTER,
+ CONSUMER_ADAPTER,
+ mock(),
+ mock(),
+ mock(),
+ )
@Suppress("Deprecation")
@Test
@@ -42,13 +51,22 @@
val callback =
object : EmbeddingInterfaceCompat.EmbeddingCallbackInterface {
override fun onSplitInfoChanged(splitInfo: List<SplitInfo>) {}
+
+ override fun onActivityStackChanged(activityStacks: List<ActivityStack>) {}
}
embeddingCompat.setEmbeddingCallback(callback)
- if (vendorApiLevel < 2) {
- verify(component).setSplitInfoCallback(any<JavaConsumer<List<OEMSplitInfo>>>())
- } else {
- verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+ when (extensionVersion) {
+ 1 -> verify(component).setSplitInfoCallback(any<JavaConsumer<List<OEMSplitInfo>>>())
+ in 2..4 -> verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+ 5 -> {
+ verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+ verify(component)
+ .registerActivityStackCallback(
+ any<Executor>(),
+ any<Consumer<List<OEMActivityStack>>>()
+ )
+ }
}
}
diff --git a/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt b/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt
new file mode 100644
index 0000000..aa2bb20
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.annotation.SuppressLint
+import android.content.res.Configuration
+import android.graphics.Rect
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.WindowSdkExtensionsRule
+import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+/** Verifies [OverlayControllerImpl] */
+@Suppress("GuardedBy")
+class OverlayControllerImplTest {
+
+ @get:Rule val testRule = WindowSdkExtensionsRule()
+
+ private lateinit var overlayController: TestableOverlayControllerImpl
+
+ @Before
+ fun setUp() {
+ testRule.overrideExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+ overlayController = TestableOverlayControllerImpl()
+ }
+
+ /** Verifies the behavior of [OverlayControllerImpl.calculateOverlayAttributes] */
+ @SuppressLint("NewApi")
+ @Test
+ fun testCalculateOverlayAttributes() {
+ assertThat(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+ .isEqualTo(DEFAULT_OVERLAY_ATTRS)
+
+ overlayController.overlayAttributesCalculator = { _ -> CALCULATED_OVERLAY_ATTRS }
+
+ assertWithMessage("Calculated overlay attrs must be reported if calculator exists.")
+ .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+ .isEqualTo(CALCULATED_OVERLAY_ATTRS)
+
+ overlayController.updateOverlayAttributes(TAG_TEST, UPDATED_OVERLAY_ATTRS)
+
+ assertWithMessage("Calculated overlay attrs must be reported if calculator exists.")
+ .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+ .isEqualTo(CALCULATED_OVERLAY_ATTRS)
+
+ overlayController.overlayAttributesCalculator = null
+
+ assertWithMessage("#updateOverlayAttributes should also update the current overlay attrs.")
+ .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+ .isEqualTo(UPDATED_OVERLAY_ATTRS)
+ }
+
+ private fun OverlayControllerImpl.calculateOverlayAttributes(
+ initialOverlayAttrs: OverlayAttributes?
+ ): OverlayAttributes =
+ calculateOverlayAttributes(
+ TAG_TEST,
+ initialOverlayAttrs,
+ WindowMetrics(Rect(), WindowInsetsCompat.CONSUMED, density = 1f),
+ Configuration(),
+ WindowLayoutInfo(emptyList())
+ )
+
+ companion object {
+ private const val TAG_TEST = "test"
+
+ private val DEFAULT_OVERLAY_ATTRS = OverlayAttributes.Builder().build()
+
+ private val CALCULATED_OVERLAY_ATTRS =
+ OverlayAttributes.Builder().setBounds(EmbeddingBounds.BOUNDS_HINGE_RIGHT).build()
+
+ private val UPDATED_OVERLAY_ATTRS =
+ OverlayAttributes.Builder().setBounds(EmbeddingBounds.BOUNDS_HINGE_BOTTOM).build()
+ }
+
+ private class TestableOverlayControllerImpl(
+ mockExtension: ActivityEmbeddingComponent = mock<ActivityEmbeddingComponent>(),
+ ) :
+ OverlayControllerImpl(
+ mockExtension,
+ EmbeddingAdapter(PredicateAdapter(ClassLoader.getSystemClassLoader()))
+ ) {
+ val overlayTagToAttributesMap = HashMap<String, OverlayAttributes>()
+
+ override fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =
+ overlayTagToAttributesMap[overlayTag]
+
+ override fun updateOverlayAttributes(
+ overlayTag: String,
+ overlayAttributes: OverlayAttributes
+ ) {
+ overlayTagToAttributesMap[overlayTag] = overlayAttributes
+ }
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt
new file mode 100644
index 0000000..c20b535
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 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.embedding
+
+import android.os.Bundle
+import androidx.core.util.Consumer
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class OverlayControllerTest {
+ private val mockBackend = mock<EmbeddingBackend>()
+ private val overlayController = OverlayController(mockBackend)
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
+ @Test
+ fun testSetOverlayCreateParams() {
+ val options = Bundle()
+ val params = OverlayCreateParams()
+ overlayController.setOverlayCreateParams(options, params)
+
+ verify(mockBackend).setOverlayCreateParams(options, params)
+ }
+
+ @Test
+ fun test_overlayAttributesCalculator_delegates() {
+ val calculator = { _: OverlayAttributesCalculatorParams -> OverlayAttributes() }
+
+ overlayController.setOverlayAttributesCalculator(calculator)
+
+ verify(mockBackend).setOverlayAttributesCalculator(calculator)
+
+ overlayController.clearOverlayAttributesCalculator()
+
+ verify(mockBackend).clearOverlayAttributesCalculator()
+ }
+
+ @Test
+ fun test_updateOverlayAttributes_delegates() {
+ val tag = "test"
+ val overlayAttributes = OverlayAttributes()
+
+ overlayController.updateOverlayAttributes(tag, overlayAttributes)
+
+ verify(mockBackend).updateOverlayAttributes(tag, overlayAttributes)
+ }
+
+ @Test
+ fun test_overlayInfoComesFromBackend() =
+ testScope.runTest {
+ val tag = "test"
+ val expected =
+ OverlayInfo(
+ overlayTag = tag,
+ currentOverlayAttributes = OverlayAttributes(),
+ activityStack =
+ ActivityStack(
+ emptyList(),
+ true,
+ ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
+ )
+ )
+ doAnswer { invocationOnMock ->
+ @Suppress("UNCHECKED_CAST")
+ val listener = invocationOnMock.arguments.last() as Consumer<OverlayInfo>
+ listener.accept(expected)
+ }
+ .whenever(mockBackend)
+ .addOverlayInfoCallback(any(), any(), any())
+
+ val actual = overlayController.overlayInfo(tag).take(1).toList().first()
+
+ Assert.assertEquals(expected, actual)
+ verify(mockBackend).addOverlayInfoCallback(eq(tag), any(), any())
+ verify(mockBackend).removeOverlayInfoCallback(any())
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
index 3cd660b..8c3d4de 100644
--- a/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
+++ b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
@@ -16,24 +16,27 @@
package androidx.window.embedding
-import android.app.ActivityOptions
import android.content.Context
-import android.os.Binder
-import android.os.Build
+import android.os.Bundle
import android.os.IBinder
-import androidx.annotation.RequiresApi
import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
import androidx.window.WindowSdkExtensionsRule
import androidx.window.core.ConsumerAdapter
import androidx.window.core.PredicateAdapter
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
import androidx.window.extensions.core.util.function.Function
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
import androidx.window.extensions.embedding.SplitAttributes as OemSplitAttributes
import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OemSplitAttributesCalculatorParams
+import androidx.window.extensions.embedding.SplitInfo.Token as SplitInfoToken
+import java.util.concurrent.Executor
import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
@@ -41,8 +44,11 @@
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
-import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -53,7 +59,7 @@
* successfully
* - Otherwise, [UnsupportedOperationException] must be thrown.
*/
-@RequiresApi(Build.VERSION_CODES.M) // To call ActivityOptions.makeBasic()
+@Suppress("Deprecation")
class RequiresWindowSdkExtensionTests {
@get:Rule val testRule = WindowSdkExtensionsRule()
@@ -61,26 +67,18 @@
@Mock private lateinit var embeddingExtension: ActivityEmbeddingComponent
@Mock private lateinit var classLoader: ClassLoader
@Mock private lateinit var applicationContext: Context
- @Mock private lateinit var activityOptions: ActivityOptions
+ @Mock private lateinit var options: Bundle
private lateinit var mockAnnotations: AutoCloseable
private lateinit var embeddingCompat: EmbeddingCompat
+ private lateinit var activityStack: ActivityStack
- @Suppress("DEPRECATION")
+ private val activityStackToken = ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
+
@Before
fun setUp() {
mockAnnotations = MockitoAnnotations.openMocks(this)
- embeddingCompat =
- EmbeddingCompat(
- embeddingExtension,
- EmbeddingAdapter(PredicateAdapter(classLoader)),
- ConsumerAdapter(classLoader),
- applicationContext
- )
-
- doReturn(activityOptions)
- .whenever(embeddingExtension)
- .setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+ activityStack = ActivityStack(emptyList(), isEmpty = true, activityStackToken)
}
@After
@@ -88,10 +86,10 @@
mockAnnotations.close()
}
- @Suppress("DEPRECATION")
@Test
- fun testVendorApiLevel1() {
+ fun testWindowExtensionsVersion1() {
testRule.overrideExtensionVersion(1)
+ createTestEmbeddingCompat()
assertThrows(UnsupportedOperationException::class.java) {
embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
@@ -104,26 +102,34 @@
verify(embeddingExtension, never()).clearSplitAttributesCalculator()
assertThrows(UnsupportedOperationException::class.java) {
- embeddingCompat.setLaunchingActivityStack(activityOptions, Binder())
+ embeddingCompat.setLaunchingActivityStack(options, activityStack)
}
- verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+ verify(options, never()).putBinder(any(), any())
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.finishActivityStacks(emptySet())
+ }
+ verify(embeddingExtension, never())
+ .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
assertThrows(UnsupportedOperationException::class.java) {
embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
}
- verify(embeddingExtension, never())
- .updateSplitAttributes(any<IBinder>(), any<OemSplitAttributes>())
+ verify(embeddingExtension, never()).updateSplitAttributes(any<IBinder>(), any())
assertThrows(UnsupportedOperationException::class.java) {
- embeddingCompat.invalidateTopVisibleSplitAttributes()
+ embeddingCompat.invalidateVisibleActivityStacks()
}
verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
}
- @Suppress("DEPRECATION")
@Test
- fun testVendorApiLevel2() {
+ fun testWindowExtensionsVersion2() {
testRule.overrideExtensionVersion(2)
+ createTestEmbeddingCompat()
embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
verify(embeddingExtension)
@@ -135,26 +141,42 @@
verify(embeddingExtension).clearSplitAttributesCalculator()
assertThrows(UnsupportedOperationException::class.java) {
- embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+ embeddingCompat.setLaunchingActivityStack(options, activityStack)
}
- verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+ verify(options, never()).putBundle(any(), any())
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.finishActivityStacks(emptySet())
+ }
+ verify(embeddingExtension, never())
+ .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
assertThrows(UnsupportedOperationException::class.java) {
embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
}
- verify(embeddingExtension, never())
- .updateSplitAttributes(any<IBinder>(), any<OemSplitAttributes>())
+ verify(embeddingExtension, never()).updateSplitAttributes(any<IBinder>(), any())
assertThrows(UnsupportedOperationException::class.java) {
- embeddingCompat.invalidateTopVisibleSplitAttributes()
+ embeddingCompat.invalidateVisibleActivityStacks()
}
verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
}
- @Suppress("DEPRECATION")
@Test
- fun testVendorApiLevel3() {
+ fun testWindowExtensionsVersion3() {
testRule.overrideExtensionVersion(3)
+ createTestEmbeddingCompat()
+
+ val splitInfo =
+ SplitInfo(
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ SplitAttributes.Builder().build(),
+ binder = INVALID_SPLIT_INFO_TOKEN,
+ )
embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
verify(embeddingExtension)
@@ -165,17 +187,228 @@
embeddingCompat.clearSplitAttributesCalculator()
verify(embeddingExtension).clearSplitAttributesCalculator()
- embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.setLaunchingActivityStack(options, activityStack)
+ }
+ verify(options, never()).putBundle(any(), any())
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.finishActivityStacks(emptySet())
+ }
+ verify(embeddingExtension, never())
+ .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
+
+ embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
verify(embeddingExtension)
- .setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+ .updateSplitAttributes(splitInfo.getBinder(), OemSplitAttributes.Builder().build())
- embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
- verify(embeddingExtension)
- .updateSplitAttributes(INVALID_SPLIT_INFO_TOKEN, OemSplitAttributes.Builder().build())
-
- embeddingCompat.invalidateTopVisibleSplitAttributes()
+ embeddingCompat.invalidateVisibleActivityStacks()
verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
+ }
+
+ @Test
+ fun testWindowExtensionsVersion4() {
+ testRule.overrideExtensionVersion(4)
+ createTestEmbeddingCompat()
+
+ val splitInfo =
+ SplitInfo(
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ SplitAttributes.Builder().build(),
+ binder = INVALID_SPLIT_INFO_TOKEN,
+ )
+
+ embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+ verify(embeddingExtension)
+ .setSplitAttributesCalculator(
+ any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+ )
+
+ embeddingCompat.clearSplitAttributesCalculator()
+ verify(embeddingExtension).clearSplitAttributesCalculator()
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.setLaunchingActivityStack(options, activityStack)
+ }
+ verify(options, never()).putBinder(any(), any())
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.finishActivityStacks(emptySet())
+ }
+ verify(embeddingExtension, never())
+ .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
+
+ embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
+ verify(embeddingExtension)
+ .updateSplitAttributes(splitInfo.getBinder(), OemSplitAttributes.Builder().build())
+
+ embeddingCompat.invalidateVisibleActivityStacks()
+ verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
+ }
+
+ @Test
+ fun testWindowExtensionsVersion5() {
+ testRule.overrideExtensionVersion(5)
+ createTestEmbeddingCompat()
+
+ val splitInfo =
+ SplitInfo(
+ ActivityStack(emptyList(), isEmpty = true),
+ ActivityStack(emptyList(), isEmpty = true),
+ SplitAttributes.Builder().build(),
+ token = SplitInfoToken.createFromBinder(INVALID_SPLIT_INFO_TOKEN),
+ )
+
+ embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+ verify(embeddingExtension)
+ .setSplitAttributesCalculator(
+ any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+ )
+
+ embeddingCompat.clearSplitAttributesCalculator()
+ verify(embeddingExtension).clearSplitAttributesCalculator()
+
+ embeddingCompat.setLaunchingActivityStack(options, activityStack)
+ verify(options)
+ .putBundle(eq(ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN), any())
+
+ embeddingCompat.finishActivityStacks(emptySet())
+ verify(embeddingExtension).finishActivityStacksWithTokens(emptySet())
+
+ embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
+ verify(embeddingExtension)
+ .updateSplitAttributes(splitInfo.getToken(), OemSplitAttributes.Builder().build())
+
+ embeddingCompat.invalidateVisibleActivityStacks()
+ verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
+ }
+
+ @Test
+ fun testWindowExtensionsVersion6() {
+ testRule.overrideExtensionVersion(6)
+ createTestEmbeddingCompat()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
+ }
+
+ @Test
+ fun testWindowExtensionsVersion7() {
+ testRule.overrideExtensionVersion(7)
+ createTestEmbeddingCompat()
+
+ verifyOverlayFeatureApis()
+ verifyActivityWindowInfoCallbackController()
+ }
+
+ private fun verifyOverlayFeatureApis() {
+ if (WindowSdkExtensions.getInstance().extensionVersion >= OVERLAY_FEATURE_VERSION) {
+ embeddingCompat.setOverlayCreateParams(options, OverlayCreateParams())
+ // Verify if the overlay tag is put to the activityOptions bundle
+ verify(options).putString(any(), any())
+
+ val calculator = { _: OverlayAttributesCalculatorParams -> OverlayAttributes() }
+ embeddingCompat.setOverlayAttributesCalculator(calculator)
+ assertEquals(
+ calculator,
+ embeddingCompat.overlayController!!.overlayAttributesCalculator
+ )
+
+ embeddingCompat.updateOverlayAttributes("", OverlayAttributes())
+ verify(embeddingCompat.overlayController)!!.updateOverlayAttributes(
+ "",
+ OverlayAttributes()
+ )
+
+ val executor = mock<Executor>()
+ embeddingCompat.addOverlayInfoCallback("", executor) {}
+ verify(embeddingCompat.overlayController)!!.addOverlayInfoCallback(
+ eq(""),
+ eq(executor),
+ any()
+ )
+
+ embeddingCompat.removeOverlayInfoCallback {}
+ verify(embeddingCompat.overlayController)!!.removeOverlayInfoCallback(any())
+ } else {
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.setOverlayCreateParams(options, OverlayCreateParams())
+ }
+ // Verify if the overlay tag is put to the activityOptions bundle
+ verify(options, never()).putString(any(), any())
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.setOverlayAttributesCalculator { _ -> OverlayAttributes() }
+ }
+ assertNull(embeddingCompat.overlayController)
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ embeddingCompat.updateOverlayAttributes("", OverlayAttributes())
+ }
+ verify(embeddingExtension, never()).updateActivityStackAttributes(any(), any())
+
+ embeddingCompat.addOverlayInfoCallback("", Runnable::run) {}
+ verify(embeddingExtension, never()).registerActivityStackCallback(any(), any())
+
+ embeddingCompat.removeOverlayInfoCallback {}
+ verify(embeddingExtension, never()).unregisterActivityStackCallback(any())
+ }
+ }
+
+ private fun verifyActivityWindowInfoCallbackController() {
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
+ ActivityWindowInfoCallbackController(embeddingExtension)
+ } else {
+ assertThrows(UnsupportedOperationException::class.java) {
+ ActivityWindowInfoCallbackController(embeddingExtension)
+ }
+ }
+ }
+
+ private fun createTestEmbeddingCompat() {
+ val overlayController =
+ if (WindowSdkExtensions.getInstance().extensionVersion >= OVERLAY_FEATURE_VERSION) {
+ spy(
+ OverlayControllerImpl(
+ embeddingExtension,
+ EmbeddingAdapter(PredicateAdapter(classLoader))
+ )
+ )
+ } else {
+ null
+ }
+ overlayController?.apply {
+ doNothing().whenever(this).updateOverlayAttributes(any(), any())
+ doNothing().whenever(this).addOverlayInfoCallback(any(), any(), any())
+ doNothing().whenever(this).removeOverlayInfoCallback(any())
+ }
+
+ val activityWindowInfoCallbackController =
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
+ spy(ActivityWindowInfoCallbackController(embeddingExtension))
+ } else {
+ null
+ }
+
+ embeddingCompat =
+ EmbeddingCompat(
+ embeddingExtension,
+ EmbeddingAdapter(PredicateAdapter(classLoader)),
+ ConsumerAdapter(classLoader),
+ applicationContext,
+ overlayController,
+ activityWindowInfoCallbackController,
+ )
}
companion object {
@@ -184,7 +417,6 @@
ActivityStack(emptyList(), isEmpty = true),
ActivityStack(emptyList(), isEmpty = true),
SplitAttributes.Builder().build(),
- INVALID_SPLIT_INFO_TOKEN,
)
private val TEST_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
index 70fa3cf..43817d5 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
@@ -32,7 +32,7 @@
@Test
fun testSplitAttributesCalculatorParams() {
- val parentWindowMetrics = WindowMetrics(Rect())
+ val parentWindowMetrics = WindowMetrics(Rect(), density = 1f)
val parentConfiguration = Configuration()
val parentWindowLayoutInfo = WindowLayoutInfo(emptyList())
val defaultSplitAttributes = SplitAttributes.Builder().build()
@@ -59,7 +59,7 @@
@Test
fun testToString() {
- val parentWindowMetrics = WindowMetrics(Rect())
+ val parentWindowMetrics = WindowMetrics(Rect(), density = 1f)
val parentConfiguration = Configuration()
val parentWindowLayoutInfo = WindowLayoutInfo(emptyList())
val defaultSplitAttributes = SplitAttributes.Builder().build()
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
index 313fd4e..9eef488 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
@@ -16,7 +16,10 @@
package androidx.window.embedding
+import android.graphics.Color
import androidx.window.core.WindowStrictModeException
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
@@ -42,16 +45,38 @@
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
val attrs2 =
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
val attrs3 =
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(TOP_TO_BOTTOM)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+ .build()
+ val animParams =
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+ )
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ val attrs4 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_HINGE)
+ .setLayoutDirection(TOP_TO_BOTTOM)
+ .setAnimationParams(animParams)
+ .build()
+ val attrs5 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_HINGE)
+ .setLayoutDirection(TOP_TO_BOTTOM)
+ .setAnimationParams(animParams)
.build()
assertNotEquals(attrs1, attrs2)
@@ -62,6 +87,62 @@
assertNotEquals(attrs3, attrs1)
assertNotEquals(attrs3.hashCode(), attrs1.hashCode())
+
+ assertNotEquals(attrs3, attrs4)
+ assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
+
+ assertEquals(attrs4, attrs5)
+ assertEquals(attrs4.hashCode(), attrs5.hashCode())
+ }
+
+ @Test
+ fun testSplitAttributesEquals_withDividerAttributes() {
+ // No divider
+ val attrs1 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+ .build()
+
+ // Fixed divider
+ val attrs2 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+ .setDividerAttributes(FixedDividerAttributes.Builder().build())
+ .build()
+
+ // Draggable divider
+ val attrs3 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+ .setDividerAttributes(DraggableDividerAttributes.Builder().build())
+ .build()
+
+ // Draggable divider same as attrs3
+ val attrs4 =
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_EQUAL)
+ .setLayoutDirection(LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+ .setDividerAttributes(DraggableDividerAttributes.Builder().build())
+ .build()
+
+ // No divider vs fixed divider
+ assertNotEquals(attrs1, attrs2)
+ assertNotEquals(attrs1.hashCode(), attrs2.hashCode())
+
+ // Fixed divider vs draggable divider
+ assertNotEquals(attrs2, attrs3)
+ assertNotEquals(attrs2.hashCode(), attrs3.hashCode())
+
+ // Same draggable divider
+ assertEquals(attrs3, attrs4)
+ assertEquals(attrs3.hashCode(), attrs4.hashCode())
}
@Test
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
index 2636fce..cca8a13 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
@@ -18,7 +18,6 @@
import android.app.Activity
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
@@ -34,7 +33,7 @@
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+@OptIn(ExperimentalCoroutinesApi::class)
internal class SplitControllerTest {
private val mockBackend = mock<EmbeddingBackend>()
@@ -50,7 +49,6 @@
ActivityStack(emptyList(), true),
ActivityStack(emptyList(), true),
SplitAttributes(),
- mock()
)
)
doAnswer { invocationOnMock ->
@@ -98,15 +96,8 @@
ActivityStack(emptyList(), true),
ActivityStack(emptyList(), true),
mockSplitAttributes,
- mock()
)
splitController.updateSplitAttributes(mockSplitInfo, mockSplitAttributes)
verify(mockBackend).updateSplitAttributes(eq(mockSplitInfo), eq(mockSplitAttributes))
}
-
- @Test
- fun test_invalidateTopVisibleSplitAttributes_delegates() {
- splitController.invalidateTopVisibleSplitAttributes()
- verify(mockBackend).invalidateTopVisibleSplitAttributes()
- }
}
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
index 71bfc69..fd77ee8 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -18,14 +18,24 @@
import android.app.Activity
import android.os.Binder
+import androidx.window.WindowSdkExtensionsRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
/** Unit tests for [SplitInfo] */
class SplitInfoTest {
+ @get:Rule val testRule = WindowSdkExtensionsRule()
+
+ @Before
+ fun setUp() {
+ testRule.overrideExtensionVersion(3)
+ }
+
@Test
fun testSplitInfoContainsActivityFirstStack() {
val activity = mock<Activity>()
@@ -95,6 +105,6 @@
private fun createTestActivityStack(
activitiesInProcess: List<Activity>,
- isEmpty: Boolean = false,
+ isEmpty: Boolean = false
): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
}
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
index 1fdfe34..35195af 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
@@ -17,6 +17,7 @@
package androidx.window.embedding
import android.content.ComponentName
+import android.graphics.Color
import android.graphics.Rect
import androidx.window.core.ActivityComponentInfo
import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
@@ -78,6 +79,7 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
TestCase.assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -101,6 +103,14 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+ )
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
filters.add(SplitPairFilter(ComponentName("a", "b"), ComponentName("c", "d"), "ACTION"))
val rule =
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
index bd5dfe5..857a895 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
@@ -18,6 +18,7 @@
import android.content.ComponentName
import android.content.Intent
+import android.graphics.Color
import android.graphics.Rect
import androidx.window.core.ActivityComponentInfo
import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
@@ -72,6 +73,7 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .setAnimationParams(EmbeddingAnimationParams.Builder().build())
.build()
assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
assertTrue(rule.checkParentBounds(density, validBounds))
@@ -91,6 +93,14 @@
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .setAnimationParams(
+ EmbeddingAnimationParams.Builder()
+ .setAnimationBackground(
+ EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+ )
+ .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+ .build()
+ )
.build()
val rule =
SplitPlaceholderRule.Builder(filters, intent)
diff --git a/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt b/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
index 1a51386..c34c857 100644
--- a/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
+++ b/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
@@ -36,43 +36,46 @@
@Test
fun testValidateReflectionSuccess() {
- val result = validateReflection { true }
+ val result = validateReflection("") { true }
assertTrue(result)
}
@Test
fun testValidateReflectionFail() {
- val result = validateReflection {
- classLoader.loadClass("SomeUnExistedClass.java")
- true
- }
+ val result =
+ validateReflection("") {
+ classLoader.loadClass("SomeUnExistedClass.java")
+ true
+ }
assertFalse(result)
}
@Test
fun testMethodModifier() {
- val result = validateReflection {
- val testClass = this::class.java
- val privateMethod = testClass.getDeclaredMethod("testMethod").isPublic
- assertFalse(privateMethod)
- val publicMethod = testClass.getDeclaredMethod("testMethodModifier").isPublic
- assertTrue(publicMethod)
- true
- }
+ val result =
+ validateReflection("") {
+ val testClass = this::class.java
+ val privateMethod = testClass.getDeclaredMethod("testMethod").isPublic
+ assertFalse(privateMethod)
+ val publicMethod = testClass.getDeclaredMethod("testMethodModifier").isPublic
+ assertTrue(publicMethod)
+ true
+ }
assertTrue(result)
}
@Test
fun testDoesReturn() {
- val result = validateReflection {
- val testClass = this::class.java
- val privateMethod = testClass.getDeclaredMethod("testMethod")
- assertTrue(privateMethod.doesReturn(Int::class.java))
- assertTrue(privateMethod.doesReturn(Int::class))
- assertFalse(privateMethod.doesReturn(Any::class.java))
- assertFalse(privateMethod.doesReturn(Any::class))
- true
- }
+ val result =
+ validateReflection("") {
+ val testClass = this::class.java
+ val privateMethod = testClass.getDeclaredMethod("testMethod")
+ assertTrue(privateMethod.doesReturn(Int::class.java))
+ assertTrue(privateMethod.doesReturn(Int::class))
+ assertFalse(privateMethod.doesReturn(Any::class.java))
+ assertFalse(privateMethod.doesReturn(Any::class))
+ true
+ }
assertTrue(result)
}
diff --git a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
index a0c2a4b..dffdc4f 100644
--- a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
+++ b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
@@ -66,7 +66,7 @@
override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
val bounds = overrideBounds ?: currentBounds[context] ?: Rect()
- return WindowMetrics(bounds)
+ return WindowMetrics(bounds, density = 1f)
}
override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
@@ -75,7 +75,7 @@
override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
val bounds = overrideMaxBounds ?: maxBounds[context] ?: Rect()
- return WindowMetrics(bounds)
+ return WindowMetrics(bounds, density = 1f)
}
/** Clears any overrides set with [.setCurrentBounds] or [.setCurrentBoundsForActivity]. */
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 7bac479..e9e5895 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -26,6 +26,7 @@
minifyEnabled = true
}
}
+ compileSdkVersion 35
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
diff --git a/work/work-datatransfer/build.gradle b/work/work-datatransfer/build.gradle
index b90d5b32..21e3a10 100644
--- a/work/work-datatransfer/build.gradle
+++ b/work/work-datatransfer/build.gradle
@@ -30,6 +30,7 @@
}
android {
+ compileSdk 35
defaultConfig {
minSdkVersion 23
}
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index a6faef5..ef6a81f0 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -30,6 +30,7 @@
}
android {
+ compileSdk 35
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 3b4bedfe..9b96902 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -30,6 +30,7 @@
}
android {
+ compileSdk 35
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/work/work-runtime-ktx/build.gradle b/work/work-runtime-ktx/build.gradle
index e7a7ef4..db3a25c 100644
--- a/work/work-runtime-ktx/build.gradle
+++ b/work/work-runtime-ktx/build.gradle
@@ -43,5 +43,6 @@
}
android {
+ compileSdk 35
namespace "androidx.work.ktx"
}
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index f4f14b5..99dd6e5 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -55,6 +55,7 @@
}
}
}
+ compileSdk = 35
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/src/schemas".toString())
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 8dd4953..6397ca8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -401,6 +401,20 @@
}
}
+ @Test
+ @SmallTest
+ public void testEnsureTraceTags() {
+ if (Build.VERSION.SDK_INT < 35) {
+ return;
+ }
+
+ final String id = "id";
+ WorkSpec workSpec = new WorkSpec(id, TestWorker.class.getName());
+ workSpec.setTraceTag(TestWorker.class.getSimpleName());
+ JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+ assertEquals(jobInfo.getTraceTag(), TestWorker.class.getSimpleName());
+ }
+
private WorkSpec getTestWorkSpecWithConstraints(Constraints constraints) {
return new OneTimeWorkRequest.Builder(TestWorker.class)
.setConstraints(constraints)
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
index f21c8f1..ae791e3 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
@@ -51,6 +51,7 @@
import java.util.concurrent.CancellationException
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
+import kotlin.collections.removeLast as removeLastKt
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
@@ -420,7 +421,7 @@
private fun iterativelyFailWorkAndDependents(workSpecId: String) {
val idsToProcess = mutableListOf(workSpecId)
while (idsToProcess.isNotEmpty()) {
- val id = idsToProcess.removeLast()
+ val id = idsToProcess.removeLastKt()
// Don't fail already cancelled work.
if (workSpecDao.getState(id) !== WorkInfo.State.CANCELLED) {
workSpecDao.setState(WorkInfo.State.FAILED, id)
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index dc462d6..158f9fc 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -139,6 +139,13 @@
//noinspection NewApi
builder.setExpedited(true);
}
+ if (Build.VERSION.SDK_INT >= 35) {
+ // Add a trace tag that shows the actual worker running.
+ String traceTag = workSpec.getTraceTag();
+ if (traceTag != null) {
+ builder.setTraceTag(traceTag);
+ }
+ }
return builder.build();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
index 4854c3d..363724e 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
@@ -25,6 +25,7 @@
import androidx.work.impl.WorkManagerImpl
import androidx.work.launchOperation
import java.util.UUID
+import kotlin.collections.removeLast as removeLastKt
private fun cancel(workManagerImpl: WorkManagerImpl, workSpecId: String) {
iterativelyCancelWorkAndDependents(workManagerImpl.workDatabase, workSpecId)
@@ -48,7 +49,7 @@
val dependencyDao = workDatabase.dependencyDao()
val idsToProcess = mutableListOf(workSpecId)
while (idsToProcess.isNotEmpty()) {
- val id = idsToProcess.removeLast()
+ val id = idsToProcess.removeLastKt()
// Don't fail already cancelled work.
val state = workSpecDao.getState(id)
if (state !== WorkInfo.State.SUCCEEDED && state !== WorkInfo.State.FAILED) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
index 47d8d5a..9e35c48 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
@@ -30,6 +30,7 @@
import androidx.work.impl.model.WorkSpec
import androidx.work.impl.workers.ARGUMENT_CLASS_NAME
import androidx.work.impl.workers.ConstraintTrackingWorker
+import kotlin.collections.removeLast as removeLastKt
internal fun checkContentUriTriggerWorkerLimits(
workDatabase: WorkDatabase,
@@ -40,7 +41,7 @@
val continuations = mutableListOf(continuation)
var newCount = 0
while (continuations.isNotEmpty()) {
- val current = continuations.removeLast()
+ val current = continuations.removeLastKt()
newCount += current.work.count { it.workSpec.constraints.hasContentUriTriggers() }
(current.parents as List<WorkContinuationImpl>?)?.let { continuations.addAll(it) }
}
diff --git a/work/work-rxjava2/build.gradle b/work/work-rxjava2/build.gradle
index 0a17f15..98670f7 100644
--- a/work/work-rxjava2/build.gradle
+++ b/work/work-rxjava2/build.gradle
@@ -50,5 +50,6 @@
}
android {
+ compileSdk 35
namespace "androidx.work.rxjava2"
}
diff --git a/work/work-rxjava3/build.gradle b/work/work-rxjava3/build.gradle
index 8457e44..f7aba04 100644
--- a/work/work-rxjava3/build.gradle
+++ b/work/work-rxjava3/build.gradle
@@ -49,5 +49,6 @@
}
android {
+ compileSdk 35
namespace "androidx.work.rxjava3"
}
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index 32597de..486b03a 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -63,5 +63,6 @@
}
android {
+ compileSdk 35
namespace "androidx.work.testing"
}